mirror of
https://github.com/cloudreve/Cloudreve.git
synced 2024-04-21 12:31:40 +00:00
Compare commits
56
Commits
3.6.0-beta1
...
3.7.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f172220825 | ||
|
|
37cb292530 | ||
|
|
835605a5cb | ||
|
|
35c4215c0f | ||
|
|
3db803ed38 | ||
|
|
c2d7168c26 | ||
|
|
b441d884f6 | ||
|
|
d4c79cb962 | ||
|
|
e134826bd1 | ||
|
|
b78f475df8 | ||
|
|
e7de7e868d | ||
|
|
a58e3b19ec | ||
|
|
71cc332109 | ||
|
|
076aa2c567 | ||
|
|
7dfe8fb439 | ||
|
|
b1b74b7be5 | ||
|
|
abe90e4c88 | ||
|
|
95027e4f5d | ||
|
|
9c58278e08 | ||
|
|
6d1c44f21b | ||
|
|
489a2bab4f | ||
|
|
d67d0512f8 | ||
|
|
1c1cd9b342 | ||
|
|
2a1e82aede | ||
|
|
a93ea2cfa0 | ||
|
|
ffbafca994 | ||
|
|
99434d7aa5 | ||
|
|
f7fdf10d70 | ||
|
|
9ad2c3508f | ||
|
|
5a8c86c72e | ||
|
|
1c922ac981 | ||
|
|
4541400755 | ||
|
|
c39daeb0d0 | ||
|
|
8dafb4f40a | ||
|
|
42a31f2fd1 | ||
|
|
ca80051a89 | ||
|
|
bc0c374f00 | ||
|
|
e4c87483d6 | ||
|
|
1227f35d3c | ||
|
|
08fa6964a9 | ||
|
|
9eafe07f4e | ||
|
|
73d0f2db9b | ||
|
|
82b4e29a80 | ||
|
|
9860ebbca9 | ||
|
|
435a03dd34 | ||
|
|
4e8ab75211 | ||
|
|
6ceb255512 | ||
|
|
74e1bd6a43 | ||
|
|
fd59d1b5ca | ||
|
|
2bb28a9845 | ||
|
|
5f4f6bd91a | ||
|
|
053e4352b4 | ||
|
|
08e4d2257a | ||
|
|
f02b6f0286 | ||
|
|
50a3917a65 | ||
|
|
8c5ba89f7d |
+1
-1
@@ -1 +1 @@
|
||||
custom: ["https://cloudreve.org/buy.php"]
|
||||
custom: ["https://cloudreve.org/pricing"]
|
||||
|
||||
@@ -7,10 +7,10 @@ jobs:
|
||||
name: Build
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- name: Set up Go 1.18
|
||||
- name: Set up Go 1.20
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: "1.18"
|
||||
go-version: "1.20"
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
@@ -21,35 +21,11 @@ jobs:
|
||||
- run: |
|
||||
git fetch --prune --unshallow --tags
|
||||
|
||||
- name: Get dependencies and build
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install gcc-mingw-w64-x86-64
|
||||
sudo apt-get -y install gcc-arm-linux-gnueabihf libc6-dev-armhf-cross
|
||||
sudo apt-get -y install gcc-aarch64-linux-gnu libc6-dev-arm64-cross
|
||||
chmod +x ./build.sh
|
||||
./build.sh -r b
|
||||
|
||||
- name: Upload binary files (windows_amd64)
|
||||
uses: actions/upload-artifact@v2
|
||||
- name: Build and Release
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
with:
|
||||
name: cloudreve_windows_amd64
|
||||
path: release/cloudreve*windows_amd64.*
|
||||
|
||||
- name: Upload binary files (linux_amd64)
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: cloudreve_linux_amd64
|
||||
path: release/cloudreve*linux_amd64.*
|
||||
|
||||
- name: Upload binary files (linux_arm)
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: cloudreve_linux_arm
|
||||
path: release/cloudreve*linux_arm.*
|
||||
|
||||
- name: Upload binary files (linux_arm64)
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: cloudreve_linux_arm64
|
||||
path: release/cloudreve*linux_arm64.*
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean --skip-validate
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
@@ -29,3 +29,5 @@ version.lock
|
||||
conf/conf.ini
|
||||
/statik/
|
||||
.vscode/
|
||||
|
||||
dist/
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
env:
|
||||
- CI=false
|
||||
- GENERATE_SOURCEMAP=false
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
- sh -c "cd assets && rm -rf build && yarn install --network-timeout 1000000 && yarn run build && cd ../ && zip -r - assets/build >assets.zip"
|
||||
builds:
|
||||
-
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
|
||||
binary: cloudreve
|
||||
|
||||
ldflags:
|
||||
- -X 'github.com/cloudreve/Cloudreve/v3/pkg/conf.BackendVersion={{.Tag}}' -X 'github.com/cloudreve/Cloudreve/v3/pkg/conf.LastCommit={{.ShortCommit}}'
|
||||
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
|
||||
goarm:
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarm: 5
|
||||
- goos: windows
|
||||
goarm: 6
|
||||
- goos: windows
|
||||
goarm: 7
|
||||
|
||||
archives:
|
||||
- format: tar.gz
|
||||
# this name template makes the OS and Arch compatible with the results of uname.
|
||||
name_template: >-
|
||||
cloudreve_{{.Tag}}_{{- .Os }}_{{ .Arch }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
# use zip for windows archives
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
|
||||
release:
|
||||
draft: true
|
||||
prerelease: auto
|
||||
target_commitish: '{{ .Commit }}'
|
||||
name_template: "{{.Version}}"
|
||||
|
||||
dockers:
|
||||
-
|
||||
dockerfile: Dockerfile
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/amd64"
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
goamd64: v1
|
||||
image_templates:
|
||||
- "cloudreve/cloudreve:{{ .Tag }}-amd64"
|
||||
-
|
||||
dockerfile: Dockerfile
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm64"
|
||||
goos: linux
|
||||
goarch: arm64
|
||||
image_templates:
|
||||
- "cloudreve/cloudreve:{{ .Tag }}-arm64"
|
||||
-
|
||||
dockerfile: Dockerfile
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm/v6"
|
||||
goos: linux
|
||||
goarch: arm
|
||||
goarm: '6'
|
||||
image_templates:
|
||||
- "cloudreve/cloudreve:{{ .Tag }}-armv6"
|
||||
-
|
||||
dockerfile: Dockerfile
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm/v7"
|
||||
goos: linux
|
||||
goarch: arm
|
||||
goarm: '7'
|
||||
image_templates:
|
||||
- "cloudreve/cloudreve:{{ .Tag }}-armv7"
|
||||
|
||||
docker_manifests:
|
||||
- name_template: "cloudreve/cloudreve:latest"
|
||||
image_templates:
|
||||
- "cloudreve/cloudreve:{{ .Tag }}-amd64"
|
||||
- "cloudreve/cloudreve:{{ .Tag }}-arm64"
|
||||
- "cloudreve/cloudreve:{{ .Tag }}-armv6"
|
||||
- "cloudreve/cloudreve:{{ .Tag }}-armv7"
|
||||
- name_template: "cloudreve/cloudreve:{{ .Tag }}"
|
||||
image_templates:
|
||||
- "cloudreve/cloudreve:{{ .Tag }}-amd64"
|
||||
- "cloudreve/cloudreve:{{ .Tag }}-arm64"
|
||||
- "cloudreve/cloudreve:{{ .Tag }}-armv6"
|
||||
- "cloudreve/cloudreve:{{ .Tag }}-armv7"
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.18.x
|
||||
node_js: "12.16.3"
|
||||
git:
|
||||
depth: 1
|
||||
before_install:
|
||||
- mkdir assets/build
|
||||
- touch assets/build/test.html
|
||||
script:
|
||||
- go test -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
before_deploy:
|
||||
- sudo apt-get update
|
||||
- sudo apt-get -y install gcc-mingw-w64-x86-64
|
||||
- sudo apt-get -y install gcc-arm-linux-gnueabihf libc6-dev-armhf-cross
|
||||
- sudo apt-get -y install gcc-aarch64-linux-gnu libc6-dev-arm64-cross
|
||||
- chmod +x ./build.sh
|
||||
- ./build.sh -r b
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key: $GITHUB_TOKEN
|
||||
file_glob: true
|
||||
file: release/*
|
||||
draft: true
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
+8
-35
@@ -1,42 +1,15 @@
|
||||
FROM golang:1.18-alpine as cloudreve_builder
|
||||
|
||||
|
||||
# install dependencies and build tools
|
||||
RUN apk update && apk add --no-cache wget curl git yarn build-base gcc abuild binutils binutils-doc gcc-doc zip
|
||||
|
||||
WORKDIR /cloudreve_builder
|
||||
RUN git clone --recurse-submodules https://github.com/cloudreve/Cloudreve.git
|
||||
|
||||
# build frontend
|
||||
WORKDIR /cloudreve_builder/Cloudreve/assets
|
||||
ENV GENERATE_SOURCEMAP false
|
||||
|
||||
RUN yarn install --network-timeout 1000000
|
||||
RUN yarn run build
|
||||
|
||||
# build backend
|
||||
WORKDIR /cloudreve_builder/Cloudreve
|
||||
RUN zip -r - assets/build >assets.zip
|
||||
RUN tag_name=$(git describe --tags) \
|
||||
&& export COMMIT_SHA=$(git rev-parse --short HEAD) \
|
||||
&& go build -a -o cloudreve -ldflags " -X 'github.com/HFO4/cloudreve/pkg/conf.BackendVersion=$tag_name' -X 'github.com/HFO4/cloudreve/pkg/conf.LastCommit=$COMMIT_SHA'"
|
||||
|
||||
|
||||
# build final image
|
||||
FROM alpine:latest
|
||||
|
||||
WORKDIR /cloudreve
|
||||
COPY cloudreve ./cloudreve
|
||||
|
||||
RUN apk update && apk add --no-cache tzdata
|
||||
|
||||
# we using the `Asia/Shanghai` timezone by default, you can do modification at your will
|
||||
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& echo "Asia/Shanghai" > /etc/timezone
|
||||
|
||||
COPY --from=cloudreve_builder /cloudreve_builder/Cloudreve/cloudreve ./
|
||||
|
||||
# prepare permissions and aria2 dir
|
||||
RUN chmod +x ./cloudreve && mkdir -p /data/aria2 && chmod -R 766 /data/aria2
|
||||
RUN apk update \
|
||||
&& apk add --no-cache tzdata \
|
||||
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& echo "Asia/Shanghai" > /etc/timezone \
|
||||
&& chmod +x ./cloudreve \
|
||||
&& mkdir -p /data/aria2 \
|
||||
&& chmod -R 766 /data/aria2
|
||||
|
||||
EXPOSE 5212
|
||||
VOLUME ["/cloudreve/uploads", "/cloudreve/avatar", "/data"]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
[中文版本](https://github.com/cloudreve/Cloudreve/blob/master/README_zh-CN.md)
|
||||
|
||||
<h1 align="center">
|
||||
<br>
|
||||
<a href="https://cloudreve.org/" alt="logo" ><img src="https://raw.githubusercontent.com/cloudreve/frontend/master/public/static/img/logo192.png" width="150"/></a>
|
||||
@@ -5,12 +7,11 @@
|
||||
Cloudreve
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
<h4 align="center">支持多家云存储驱动的公有云文件系统.</h4>
|
||||
<h4 align="center">Self-hosted file management system with muilt-cloud support.</h4>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/cloudreve/Cloudreve/actions/workflows/test.yml">
|
||||
<img src="https://img.shields.io/github/workflow/status/cloudreve/Cloudreve/Test?style=flat-square"
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/cloudreve/Cloudreve/test.yml?branch=master&style=flat-square"
|
||||
alt="GitHub Test Workflow">
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/cloudreve/Cloudreve"><img src="https://img.shields.io/codecov/c/github/cloudreve/Cloudreve?style=flat-square"></a>
|
||||
@@ -24,108 +25,80 @@
|
||||
<img src="https://img.shields.io/docker/image-size/cloudreve/cloudreve?style=flat-square"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://demo.cloudreve.org">演示站</a> •
|
||||
<a href="https://forum.cloudreve.org/">讨论社区</a> •
|
||||
<a href="https://docs.cloudreve.org/">文档</a> •
|
||||
<a href="https://github.com/cloudreve/Cloudreve/releases">下载</a> •
|
||||
<a href="https://t.me/cloudreve_official">Telegram 群组</a> •
|
||||
<a href="#scroll-许可证">许可证</a>
|
||||
<a href="https://cloudreve.org">Homepage</a> •
|
||||
<a href="https://demo.cloudreve.org">Demo</a> •
|
||||
<a href="https://forum.cloudreve.org/">Discussion</a> •
|
||||
<a href="https://docs.cloudreve.org/v/en/">Documents</a> •
|
||||
<a href="https://github.com/cloudreve/Cloudreve/releases">Download</a> •
|
||||
<a href="https://t.me/cloudreve_official">Telegram Group</a> •
|
||||
<a href="#scroll-License">License</a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
## :sparkles: 特性
|
||||
## :sparkles: Features
|
||||
|
||||
* :cloud: 支持本机、从机、七牛、阿里云 OSS、腾讯云 COS、又拍云、OneDrive (包括世纪互联版) 作为存储端
|
||||
* :outbox_tray: 上传/下载 支持客户端直传,支持下载限速
|
||||
* 💾 可对接 Aria2 离线下载,可使用多个从机节点分担下载任务
|
||||
* 📚 在线 压缩/解压缩、多文件打包下载
|
||||
* 💻 覆盖全部存储策略的 WebDAV 协议支持
|
||||
* :zap: 拖拽上传、目录上传、流式上传处理
|
||||
* :card_file_box: 文件拖拽管理
|
||||
* :family_woman_girl_boy: 多用户、用户组
|
||||
* :link: 创建文件、目录的分享链接,可设定自动过期
|
||||
* :eye_speech_bubble: 视频、图像、音频、文本、Office 文档在线预览
|
||||
* :art: 自定义配色、黑暗模式、PWA 应用、全站单页应用
|
||||
* :rocket: All-In-One 打包,开箱即用
|
||||
* :cloud: Support storing files into Local storage, Remote storage, Qiniu, Aliyun OSS, Tencent COS, Upyun, OneDrive, S3 compatible API.
|
||||
* :outbox_tray: Upload/Download in directly transmission with speed limiting support.
|
||||
* 💾 Integrate with Aria2 to download files offline, use multiple download nodes to share the load.
|
||||
* 📚 Compress/Extract files, download files in batch.
|
||||
* 💻 WebDAV support covering all storage providers.
|
||||
* :zap:Drag&Drop to upload files or folders, with streaming upload processing.
|
||||
* :card_file_box: Drag & Drop to manage your files.
|
||||
* :family_woman_girl_boy: Multi-users with multi-groups.
|
||||
* :link: Create share links for files and folders with expiration date.
|
||||
* :eye_speech_bubble: Preview videos, images, audios, ePub files online; edit texts, Office documents online.
|
||||
* :art: Customize theme colors, dark mode, PWA application, SPA, i18n.
|
||||
* :rocket: All-In-One packing, with all features out-of-the-box.
|
||||
* 🌈 ... ...
|
||||
|
||||
## :hammer_and_wrench: 部署
|
||||
## :hammer_and_wrench: Deploy
|
||||
|
||||
下载适用于您目标机器操作系统、CPU架构的主程序,直接运行即可。
|
||||
Download the main binary for your target machine OS, CPU architecture and run it directly.
|
||||
|
||||
```shell
|
||||
# 解压程序包
|
||||
# Extract Cloudreve binary
|
||||
tar -zxvf cloudreve_VERSION_OS_ARCH.tar.gz
|
||||
|
||||
# 赋予执行权限
|
||||
# Grant execute permission
|
||||
chmod +x ./cloudreve
|
||||
|
||||
# 启动 Cloudreve
|
||||
# Start Cloudreve
|
||||
./cloudreve
|
||||
```
|
||||
|
||||
以上为最简单的部署示例,您可以参考 [文档 - 起步](https://docs.cloudreve.org/) 进行更为完善的部署。
|
||||
The above is a minimum deploy example, you can refer to [Getting started](https://docs.cloudreve.org/v/en/getting-started/install) for a completed deployment.
|
||||
|
||||
## :gear: 构建
|
||||
## :gear: Build
|
||||
|
||||
自行构建前需要拥有 `Go >= 1.18`、`node.js`、`yarn`、`zip` 等必要依赖。
|
||||
You need to have `Go >= 1.18`, `node.js`, `yarn`, `zip`, [goreleaser](https://goreleaser.com/intro/) and other necessary dependencies before you can build it yourself.
|
||||
|
||||
#### 克隆代码
|
||||
#### Install goreleaser
|
||||
|
||||
```shell
|
||||
go install github.com/goreleaser/goreleaser@latest
|
||||
```
|
||||
|
||||
#### Clone the code
|
||||
|
||||
```shell
|
||||
git clone --recurse-submodules https://github.com/cloudreve/Cloudreve.git
|
||||
```
|
||||
|
||||
#### 构建静态资源
|
||||
#### Compile
|
||||
|
||||
```shell
|
||||
# 进入前端子模块
|
||||
cd assets
|
||||
# 安装依赖
|
||||
yarn install
|
||||
# 开始构建
|
||||
yarn run build
|
||||
# 构建完成后删除映射文件
|
||||
cd build
|
||||
find . -name "*.map" -type f -delete
|
||||
# 返回项目主目录打包静态资源
|
||||
cd ../../
|
||||
zip -r - assets/build >assets.zip
|
||||
goreleaser build --clean --single-target --snapshot
|
||||
```
|
||||
|
||||
#### 编译项目
|
||||
|
||||
```shell
|
||||
# 获得当前版本号、Commit
|
||||
export COMMIT_SHA=$(git rev-parse --short HEAD)
|
||||
export VERSION=$(git describe --tags)
|
||||
|
||||
# 开始编译
|
||||
go build -a -o cloudreve -ldflags "-s -w -X 'github.com/cloudreve/Cloudreve/v3/pkg/conf.BackendVersion=$VERSION' -X 'github.com/cloudreve/Cloudreve/v3/pkg/conf.LastCommit=$COMMIT_SHA'"
|
||||
```
|
||||
|
||||
你也可以使用项目根目录下的 `build.sh` 快速开始构建:
|
||||
|
||||
```shell
|
||||
./build.sh [-a] [-c] [-b] [-r]
|
||||
a - 构建静态资源
|
||||
c - 编译二进制文件
|
||||
b - 构建前端 + 编译二进制文件
|
||||
r - 交叉编译,构建用于release的版本
|
||||
```
|
||||
|
||||
## :alembic: 技术栈
|
||||
## :alembic: Stacks
|
||||
|
||||
* [Go](https://golang.org/) + [Gin](https://github.com/gin-gonic/gin)
|
||||
* [React](https://github.com/facebook/react) + [Redux](https://github.com/reduxjs/redux) + [Material-UI](https://github.com/mui-org/material-ui)
|
||||
|
||||
## :scroll: 许可证
|
||||
## :scroll: License
|
||||
|
||||
GPL V3
|
||||
|
||||
---
|
||||
> GitHub [@HFO4](https://github.com/HFO4) ·
|
||||
> Twitter [@abslant00](https://twitter.com/abslant00)
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
[English Version](https://github.com/cloudreve/Cloudreve/blob/master/README.md)
|
||||
|
||||
<h1 align="center">
|
||||
<br>
|
||||
<a href="https://cloudreve.org/" alt="logo" ><img src="https://raw.githubusercontent.com/cloudreve/frontend/master/public/static/img/logo192.png" width="150"/></a>
|
||||
<br>
|
||||
Cloudreve
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
<h4 align="center">支持多家云存储驱动的公有云文件系统.</h4>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/cloudreve/Cloudreve/actions/workflows/test.yml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/cloudreve/Cloudreve/test.yml?branch=master&style=flat-square"
|
||||
alt="GitHub Test Workflow">
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/cloudreve/Cloudreve"><img src="https://img.shields.io/codecov/c/github/cloudreve/Cloudreve?style=flat-square"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/cloudreve/Cloudreve">
|
||||
<img src="https://goreportcard.com/badge/github.com/cloudreve/Cloudreve?style=flat-square">
|
||||
</a>
|
||||
<a href="https://github.com/cloudreve/Cloudreve/releases">
|
||||
<img src="https://img.shields.io/github/v/release/cloudreve/Cloudreve?include_prereleases&style=flat-square" />
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/cloudreve/cloudreve">
|
||||
<img src="https://img.shields.io/docker/image-size/cloudreve/cloudreve?style=flat-square"/>
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://cloudreve.org">主页</a> •
|
||||
<a href="https://demo.cloudreve.org">演示站</a> •
|
||||
<a href="https://forum.cloudreve.org/">讨论社区</a> •
|
||||
<a href="https://docs.cloudreve.org/">文档</a> •
|
||||
<a href="https://github.com/cloudreve/Cloudreve/releases">下载</a> •
|
||||
<a href="https://t.me/cloudreve_official">Telegram 群组</a> •
|
||||
<a href="#scroll-许可证">许可证</a>
|
||||
</p>
|
||||
|
||||
|
||||

|
||||
|
||||
## :sparkles: 特性
|
||||
|
||||
* :cloud: 支持本机、从机、七牛、阿里云 OSS、腾讯云 COS、又拍云、OneDrive (包括世纪互联版) 、S3兼容协议 作为存储端
|
||||
* :outbox_tray: 上传/下载 支持客户端直传,支持下载限速
|
||||
* 💾 可对接 Aria2 离线下载,可使用多个从机节点分担下载任务
|
||||
* 📚 在线 压缩/解压缩、多文件打包下载
|
||||
* 💻 覆盖全部存储策略的 WebDAV 协议支持
|
||||
* :zap: 拖拽上传、目录上传、流式上传处理
|
||||
* :card_file_box: 文件拖拽管理
|
||||
* :family_woman_girl_boy: 多用户、用户组、多存储策略
|
||||
* :link: 创建文件、目录的分享链接,可设定自动过期
|
||||
* :eye_speech_bubble: 视频、图像、音频、 ePub 在线预览,文本、Office 文档在线编辑
|
||||
* :art: 自定义配色、黑暗模式、PWA 应用、全站单页应用、国际化支持
|
||||
* :rocket: All-In-One 打包,开箱即用
|
||||
* 🌈 ... ...
|
||||
|
||||
## :hammer_and_wrench: 部署
|
||||
|
||||
下载适用于您目标机器操作系统、CPU架构的主程序,直接运行即可。
|
||||
|
||||
```shell
|
||||
# 解压程序包
|
||||
tar -zxvf cloudreve_VERSION_OS_ARCH.tar.gz
|
||||
|
||||
# 赋予执行权限
|
||||
chmod +x ./cloudreve
|
||||
|
||||
# 启动 Cloudreve
|
||||
./cloudreve
|
||||
```
|
||||
|
||||
以上为最简单的部署示例,您可以参考 [文档 - 起步](https://docs.cloudreve.org/) 进行更为完善的部署。
|
||||
|
||||
## :gear: 构建
|
||||
|
||||
自行构建前需要拥有 `Go >= 1.18`、`node.js`、`yarn`、`zip`, [goreleaser](https://goreleaser.com/intro/) 等必要依赖。
|
||||
|
||||
#### 安装 goreleaser
|
||||
|
||||
```shell
|
||||
go install github.com/goreleaser/goreleaser@latest
|
||||
```
|
||||
|
||||
#### 克隆代码
|
||||
|
||||
```shell
|
||||
git clone --recurse-submodules https://github.com/cloudreve/Cloudreve.git
|
||||
```
|
||||
|
||||
#### 编译项目
|
||||
|
||||
```shell
|
||||
goreleaser build --clean --single-target --snapshot
|
||||
```
|
||||
|
||||
## :alembic: 技术栈
|
||||
|
||||
* [Go](https://golang.org/) + [Gin](https://github.com/gin-gonic/gin)
|
||||
* [React](https://github.com/facebook/react) + [Redux](https://github.com/reduxjs/redux) + [Material-UI](https://github.com/mui-org/material-ui)
|
||||
|
||||
## :scroll: 许可证
|
||||
|
||||
GPL V3
|
||||
+1
-1
Submodule assets updated: 00d3552a76...9f847f466c
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/email"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/mq"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/task"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/wopi"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io/fs"
|
||||
)
|
||||
@@ -95,6 +96,12 @@ func Init(path string, statics fs.FS) {
|
||||
auth.Init()
|
||||
},
|
||||
},
|
||||
{
|
||||
"master",
|
||||
func() {
|
||||
wopi.Init()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, dependency := range dependencies {
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
REPO=$(
|
||||
cd $(dirname $0)
|
||||
pwd
|
||||
)
|
||||
COMMIT_SHA=$(git rev-parse --short HEAD)
|
||||
VERSION=$(git describe --tags)
|
||||
ASSETS="false"
|
||||
BINARY="false"
|
||||
RELEASE="false"
|
||||
|
||||
debugInfo() {
|
||||
echo "Repo: $REPO"
|
||||
echo "Build assets: $ASSETS"
|
||||
echo "Build binary: $BINARY"
|
||||
echo "Release: $RELEASE"
|
||||
echo "Version: $VERSION"
|
||||
echo "Commit: $COMMIT_SHA"
|
||||
}
|
||||
|
||||
buildAssets() {
|
||||
cd $REPO
|
||||
rm -rf assets/build
|
||||
|
||||
export CI=false
|
||||
export GENERATE_SOURCEMAP=false
|
||||
|
||||
cd $REPO/assets
|
||||
|
||||
yarn install
|
||||
yarn run build
|
||||
cd build
|
||||
cd $REPO
|
||||
zip -r - assets/build >assets.zip
|
||||
}
|
||||
|
||||
buildBinary() {
|
||||
cd $REPO
|
||||
go build -a -o cloudreve -ldflags " -X 'github.com/cloudreve/Cloudreve/v3/pkg/conf.BackendVersion=$VERSION' -X 'github.com/cloudreve/Cloudreve/v3/pkg/conf.LastCommit=$COMMIT_SHA'"
|
||||
}
|
||||
|
||||
_build() {
|
||||
local osarch=$1
|
||||
IFS=/ read -r -a arr <<<"$osarch"
|
||||
os="${arr[0]}"
|
||||
arch="${arr[1]}"
|
||||
gcc="${arr[2]}"
|
||||
|
||||
# Go build to build the binary.
|
||||
export GOOS=$os
|
||||
export GOARCH=$arch
|
||||
export CC=$gcc
|
||||
export CGO_ENABLED=1
|
||||
|
||||
if [ -n "$VERSION" ]; then
|
||||
out="release/cloudreve_${VERSION}_${os}_${arch}"
|
||||
else
|
||||
out="release/cloudreve_${COMMIT_SHA}_${os}_${arch}"
|
||||
fi
|
||||
|
||||
go build -a -o "${out}" -ldflags " -X 'github.com/cloudreve/Cloudreve/v3/pkg/conf.BackendVersion=$VERSION' -X 'github.com/cloudreve/Cloudreve/v3/pkg/conf.LastCommit=$COMMIT_SHA'"
|
||||
|
||||
if [ "$os" = "windows" ]; then
|
||||
mv $out release/cloudreve.exe
|
||||
zip -j -q "${out}.zip" release/cloudreve.exe
|
||||
rm -f "release/cloudreve.exe"
|
||||
else
|
||||
mv $out release/cloudreve
|
||||
tar -zcvf "${out}.tar.gz" -C release cloudreve
|
||||
rm -f "release/cloudreve"
|
||||
fi
|
||||
}
|
||||
|
||||
release() {
|
||||
cd $REPO
|
||||
## List of architectures and OS to test coss compilation.
|
||||
SUPPORTED_OSARCH="linux/amd64/gcc linux/arm/arm-linux-gnueabihf-gcc windows/amd64/x86_64-w64-mingw32-gcc linux/arm64/aarch64-linux-gnu-gcc"
|
||||
|
||||
echo "Release builds for OS/Arch/CC: ${SUPPORTED_OSARCH}"
|
||||
for each_osarch in ${SUPPORTED_OSARCH}; do
|
||||
_build "${each_osarch}"
|
||||
done
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [-a] [-c] [-b] [-r]" 1>&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
while getopts "bacrd" o; do
|
||||
case "${o}" in
|
||||
b)
|
||||
ASSETS="true"
|
||||
BINARY="true"
|
||||
;;
|
||||
a)
|
||||
ASSETS="true"
|
||||
;;
|
||||
c)
|
||||
BINARY="true"
|
||||
;;
|
||||
r)
|
||||
ASSETS="true"
|
||||
RELEASE="true"
|
||||
;;
|
||||
d)
|
||||
DEBUG="true"
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift $((OPTIND - 1))
|
||||
|
||||
if [ "$DEBUG" = "true" ]; then
|
||||
debugInfo
|
||||
fi
|
||||
|
||||
if [ "$ASSETS" = "true" ]; then
|
||||
buildAssets
|
||||
fi
|
||||
|
||||
if [ "$BINARY" = "true" ]; then
|
||||
buildBinary
|
||||
fi
|
||||
|
||||
if [ "$RELEASE" = "true" ]; then
|
||||
release
|
||||
fi
|
||||
@@ -10,12 +10,13 @@ require (
|
||||
github.com/fatih/color v1.9.0
|
||||
github.com/gin-contrib/cors v1.3.0
|
||||
github.com/gin-contrib/gzip v0.0.2-0.20200226035851-25bef2ef21e8
|
||||
github.com/gin-contrib/sessions v0.0.1
|
||||
github.com/gin-contrib/sessions v0.0.5
|
||||
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2
|
||||
github.com/gin-gonic/gin v1.7.7
|
||||
github.com/gin-gonic/gin v1.8.1
|
||||
github.com/glebarez/go-sqlite v1.20.3
|
||||
github.com/go-ini/ini v1.50.0
|
||||
github.com/go-mail/mail v2.3.1+incompatible
|
||||
github.com/go-playground/validator/v10 v10.8.0
|
||||
github.com/go-playground/validator/v10 v10.11.0
|
||||
github.com/gofrs/uuid v4.0.0+incompatible
|
||||
github.com/gomodule/redigo v2.0.0+incompatible
|
||||
github.com/google/go-querystring v1.0.0
|
||||
@@ -31,7 +32,7 @@ require (
|
||||
github.com/rafaeljusto/redigomock v0.0.0-20191117212112-00b2509252a1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/speps/go-hashids v2.0.0+incompatible
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/stretchr/testify v1.7.2
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/captcha v1.0.393
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.393
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/scf v1.0.393
|
||||
@@ -59,7 +60,7 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 // indirect
|
||||
github.com/dsnet/compress v0.0.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v0.6.1 // indirect
|
||||
github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect
|
||||
@@ -69,6 +70,7 @@ require (
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||
github.com/goccy/go-json v0.9.8 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.1.0 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
@@ -78,11 +80,11 @@ require (
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/google/certificate-transparency-go v1.1.2-0.20210511102531-373a877eec92 // indirect
|
||||
github.com/google/go-cmp v0.5.5 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/gorilla/sessions v1.1.3 // indirect
|
||||
github.com/gorilla/sessions v1.2.1 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||
@@ -91,29 +93,30 @@ require (
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.3.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||
github.com/json-iterator/go v1.1.11 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.15.1 // indirect
|
||||
github.com/klauspost/pgzip v1.2.5 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/lib/pq v1.10.1 // indirect
|
||||
github.com/lib/pq v1.10.3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.12 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.7 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mozillazg/go-httpheader v0.2.1 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.2 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.14 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.10.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.24.0 // indirect
|
||||
github.com/prometheus/procfs v0.6.0 // indirect
|
||||
github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438 // indirect
|
||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/satori/go.uuid v1.2.0 // indirect
|
||||
@@ -124,7 +127,7 @@ require (
|
||||
github.com/stretchr/objx v0.2.0 // indirect
|
||||
github.com/therootcompany/xz v1.0.1 // indirect
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
|
||||
github.com/ugorji/go/codec v1.1.7 // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
github.com/ulikunitz/xz v0.5.10 // indirect
|
||||
github.com/urfave/cli v1.22.5 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
@@ -142,24 +145,28 @@ require (
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.7.0 // indirect
|
||||
go.uber.org/zap v1.16.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
|
||||
golang.org/x/mod v0.4.2 // indirect
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect
|
||||
golang.org/x/net v0.0.0-20220630215102-69896b714898 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/sys v0.0.0-20211020174200-9d6173849985 // indirect
|
||||
golang.org/x/sys v0.4.0 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/tools v0.1.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20210510173355-fb37daa5cd7a // indirect
|
||||
google.golang.org/grpc v1.37.0 // indirect
|
||||
google.golang.org/protobuf v1.26.0 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
|
||||
gopkg.in/mail.v2 v2.3.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.22.2 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/sqlite v1.20.3 // indirect
|
||||
sigs.k8s.io/yaml v1.2.0 // indirect
|
||||
|
||||
)
|
||||
|
||||
@@ -135,8 +135,6 @@ github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4
|
||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
|
||||
github.com/caarlos0/ctrlc v1.0.0/go.mod h1:CdXpj4rmq0q/1Eb44M9zi2nKB0QraNKuRGYGrrHhcQw=
|
||||
github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e/go.mod h1:9IOqJGCPMSc6E5ydlp5NIonxObaeu/Iub/X03EKPVYo=
|
||||
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
|
||||
@@ -149,7 +147,6 @@ github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA
|
||||
github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
|
||||
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s=
|
||||
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
@@ -183,14 +180,12 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
|
||||
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||
@@ -216,8 +211,9 @@ github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdf
|
||||
github.com/duo-labs/webauthn v0.0.0-20220330035159-03696f3d4499 h1:jaQHuGKk9NVcfu9VbA7ygslr/7utxdYs47i4osBhZP8=
|
||||
github.com/duo-labs/webauthn v0.0.0-20220330035159-03696f3d4499/go.mod h1:UMk1JMDgQDcdI2vQz+WJOIUTSjIq07qSepAVgc93rUc=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
@@ -263,8 +259,8 @@ github.com/gin-contrib/cors v1.3.0 h1:PolezCc89peu+NgkIWt9OB01Kbzt6IP0J/JvkG6xxl
|
||||
github.com/gin-contrib/cors v1.3.0/go.mod h1:artPvLlhkF7oG06nK8v3U8TNz6IeX+w1uzCSEId5/Vc=
|
||||
github.com/gin-contrib/gzip v0.0.2-0.20200226035851-25bef2ef21e8 h1:/DnKeA2+K83hkii3nqMJ5koknI+/qlojjxgcSyiAyJw=
|
||||
github.com/gin-contrib/gzip v0.0.2-0.20200226035851-25bef2ef21e8/go.mod h1:M+xPw/lXk+uAU4iYVnwPZs0iIpR/KwSQSXcJabN+gPs=
|
||||
github.com/gin-contrib/sessions v0.0.1 h1:xr9V/u3ERQnkugKSY/u36cNnC4US4bHJpdxcB6eIZLk=
|
||||
github.com/gin-contrib/sessions v0.0.1/go.mod h1:iziXm/6pvTtf7og1uxT499sel4h3S9DfwsrhNZ+REXM=
|
||||
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
|
||||
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
|
||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
@@ -272,10 +268,11 @@ github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2 h1:xLG16iua01X7
|
||||
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2/go.mod h1:VhW/Ch/3FhimwZb8Oj+qJmdMmoB8r7lmJ5auRjm50oQ=
|
||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
||||
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
|
||||
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
|
||||
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
|
||||
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4=
|
||||
github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
@@ -300,9 +297,9 @@ github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEK
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-playground/validator/v10 v10.8.0 h1:1kAa0fCrnpv+QYdkdcRzrRM7AyYs5o8+jZdJCz9xj6k=
|
||||
github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk=
|
||||
github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
|
||||
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
@@ -310,6 +307,8 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/goccy/go-json v0.9.8 h1:DxXB6MLd6yyel7CLph8EwNIonUtVZd3Ue5iRcL4DQCE=
|
||||
github.com/goccy/go-json v0.9.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||
@@ -385,8 +384,9 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM=
|
||||
github.com/google/go-licenses v0.0.0-20210329231322-ce1d9163b77d/go.mod h1:+TYOmkVoJOpwnS0wfdsJCV9CoD5nJYsHoFk/0CrTK4M=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
@@ -410,6 +410,7 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0/go.mod h1:RaTPr0KUf2K7fnZYLNDrr8rxAamWs3iNywJLtQ2AzBg=
|
||||
github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
@@ -438,8 +439,8 @@ github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
|
||||
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
@@ -508,8 +509,8 @@ github.com/jinzhu/gorm v1.9.11 h1:gaHGvE+UnWGlbWG4Y3FUwY1EcZ5n6S9WtqBA/uySMLE=
|
||||
github.com/jinzhu/gorm v1.9.11/go.mod h1:bu/pK8szGZ2puuErfU0RwyeNdsf3e6nCX/noXaVxkfw=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
|
||||
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
|
||||
@@ -527,8 +528,9 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
|
||||
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
@@ -538,7 +540,6 @@ github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSg
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
@@ -567,14 +568,14 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/go-gypsy v1.0.0/go.mod h1:chkXM0zjdpXOiqkCW1XcCHDfjfk14PH2KKkQWxfJUcU=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/letsencrypt/pkcs11key/v4 v4.0.0/go.mod h1:EFUvBDay26dErnNb70Nd0/VW3tJiIbETBPTl9ATXQag=
|
||||
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.1 h1:6VXZrLU0jHBYyAqrSPa+MgPfnSvTPuMgK+k0o5kVFWo=
|
||||
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg=
|
||||
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
|
||||
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
||||
github.com/lyft/protoc-gen-star v0.5.1/go.mod h1:9toiA3cC7z5uVbODF7kEQ91Xn7XNFkVUl+SrEe+ZORU=
|
||||
@@ -594,8 +595,8 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
@@ -604,12 +605,11 @@ github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC
|
||||
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA=
|
||||
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/mholt/archiver/v4 v4.0.0-alpha.6 h1:3wvos9Kn1GpKNBz+MpozinGREPslLo1ds1W16vTkErQ=
|
||||
github.com/mholt/archiver/v4 v4.0.0-alpha.6/go.mod h1:9PTygYq90FQBWPspdwAng6dNjYiBuTYKqmA6c15KuCo=
|
||||
@@ -632,8 +632,9 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/mojocn/base64Captcha v0.0.0-20190801020520-752b1cd608b2 h1:daZqE/T/yEoKIQNd3rwNeLsiS0VpZFfJulR0t/rtgAE=
|
||||
github.com/mojocn/base64Captcha v0.0.0-20190801020520-752b1cd608b2/go.mod h1:wAQCKEc5bDujxKRmbT6/vTnTt5CjStQ8bRfPWUuz/iY=
|
||||
@@ -687,9 +688,10 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw=
|
||||
github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI=
|
||||
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
|
||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE=
|
||||
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
@@ -748,11 +750,14 @@ github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdk
|
||||
github.com/qiniu/go-sdk/v7 v7.11.1 h1:/LZ9rvFS4p6SnszhGv11FNB1+n4OZvBCwFg7opH5Ovs=
|
||||
github.com/qiniu/go-sdk/v7 v7.11.1/go.mod h1:btsaOc8CA3hdVloULfFdDgDc+g4f3TDZEFsDY0BLE+w=
|
||||
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
|
||||
github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438 h1:jnz/4VenymvySjE+Ez511s0pqVzkUOmr1fwCVytNNWk=
|
||||
github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
|
||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc=
|
||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
|
||||
github.com/rafaeljusto/redigomock v0.0.0-20191117212112-00b2509252a1 h1:leEwA4MD1ew0lNgzz6Q4G76G3AEfeci+TMggN6WuFRs=
|
||||
github.com/rafaeljusto/redigomock v0.0.0-20191117212112-00b2509252a1/go.mod h1:JaY6n2sDr+z2WTsXkOmNRUfDy6FN0L6Nk7x06ndm4tY=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 h1:VstopitMQi3hZP0fzvnsLmzXZdQGc4bEcgu24cp+d4M=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
@@ -766,7 +771,6 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
@@ -835,8 +839,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/captcha v1.0.393 h1:hfhmMk7j4uDMRkfrrIOneMVXPBEhy3HSYiWX0gWoyhc=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/captcha v1.0.393/go.mod h1:482ndbWuXqgStZNCqE88UoZeDveIt0juS7MY71Vangg=
|
||||
@@ -859,11 +864,12 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
|
||||
@@ -901,7 +907,6 @@ go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
|
||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738 h1:VcrIfasaLFkyjk6KNlXQSzO+B0fZcnECiDrKJsfxka0=
|
||||
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
||||
go.etcd.io/etcd/api/v3 v3.5.0-alpha.0 h1:+e5nrluATIy3GP53znpkHMFzPTHGYyzvJGFCbuI6ZLc=
|
||||
go.etcd.io/etcd/api/v3 v3.5.0-alpha.0/go.mod h1:mPcW6aZJukV6Aa81LSKpBjQXTWlXB5r74ymPoSWa3Sw=
|
||||
@@ -968,8 +973,10 @@ golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -1060,8 +1067,10 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I=
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220630215102-69896b714898 h1:K7wO6V1IrczY9QOQ2WkVpw4JQSwCd52UsxVEirZUfiw=
|
||||
golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1122,7 +1131,6 @@ golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -1157,8 +1165,11 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210511113859-b0526f3d8744/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211020174200-9d6173849985 h1:LOlKVhfDyahgmqa97awczplwkjzNaELFg3zRIJ13RYo=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211020174200-9d6173849985/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1377,8 +1388,9 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.25.1-0.20200805231151-a709e31e5d12/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||
@@ -1417,8 +1429,9 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
@@ -1429,6 +1442,14 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.1.4 h1:SadWOkti5uVN1FAMgxn165+Mw00fuQKyk4Gyn/inxNQ=
|
||||
honnef.co/go/tools v0.1.4/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
|
||||
modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0=
|
||||
modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/sqlite v1.20.3 h1:SqGJMMxjj1PHusLxdYxeQSodg7Jxn9WWkaAQjKrntZs=
|
||||
modernc.org/sqlite v1.20.3/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
|
||||
pack.ag/amqp v0.11.2/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
|
||||
@@ -42,7 +42,11 @@ func init() {
|
||||
|
||||
func main() {
|
||||
// 关闭数据库连接
|
||||
defer model.DB.Close()
|
||||
defer func() {
|
||||
if model.DB != nil {
|
||||
model.DB.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if isEject {
|
||||
// 开始导出内置静态资源文件
|
||||
@@ -57,6 +61,7 @@ func main() {
|
||||
}
|
||||
|
||||
api := routers.InitRouter()
|
||||
api.TrustedPlatform = conf.SystemConfig.ProxyHeader
|
||||
server := &http.Server{Handler: api}
|
||||
|
||||
// 收到信号后关闭服务器
|
||||
@@ -98,7 +103,6 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
api.TrustedPlatform = conf.UnixConfig.ProxyHeader
|
||||
util.Log().Info("Listening to %q", conf.UnixConfig.Listen)
|
||||
if err := RunUnix(server); err != nil {
|
||||
util.Log().Error("Failed to listen to %q: %s", conf.UnixConfig.Listen, err)
|
||||
@@ -118,8 +122,21 @@ func RunUnix(server *http.Server) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer listener.Close()
|
||||
defer os.Remove(conf.UnixConfig.Listen)
|
||||
|
||||
if conf.UnixConfig.Perm > 0 {
|
||||
err = os.Chmod(conf.UnixConfig.Listen, os.FileMode(conf.UnixConfig.Perm))
|
||||
if err != nil {
|
||||
util.Log().Warning(
|
||||
"Failed to set permission to %q for socket file %q: %s",
|
||||
conf.UnixConfig.Perm,
|
||||
conf.UnixConfig.Listen,
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return server.Serve(listener)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// HashID 将给定对象的HashID转换为真实ID
|
||||
@@ -51,3 +54,24 @@ func Sandbox() gin.HandlerFunc {
|
||||
c.Header("Content-Security-Policy", "sandbox")
|
||||
}
|
||||
}
|
||||
|
||||
// StaticResourceCache 使用静态资源缓存策略
|
||||
func StaticResourceCache() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", model.GetIntSetting("public_resource_maxage", 86400)))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// MobileRequestOnly
|
||||
func MobileRequestOnly() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if c.GetHeader(auth.CrHeaderPrefix+"ios") == "" {
|
||||
c.Redirect(http.StatusMovedPermanently, model.GetSiteURL().String())
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,3 +85,21 @@ func TestCacheControl(t *testing.T) {
|
||||
TestFunc(c)
|
||||
a.Contains(c.Writer.Header().Get("Cache-Control"), "no-cache")
|
||||
}
|
||||
|
||||
func TestSandbox(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
TestFunc := Sandbox()
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
TestFunc(c)
|
||||
a.Contains(c.Writer.Header().Get("Content-Security-Policy"), "sandbox")
|
||||
}
|
||||
|
||||
func TestStaticResourceCache(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
TestFunc := StaticResourceCache()
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
TestFunc(c)
|
||||
a.Contains(c.Writer.Header().Get("Cache-Control"), "public, max-age")
|
||||
}
|
||||
|
||||
+23
-2
@@ -1,6 +1,9 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
@@ -28,9 +31,27 @@ func Session(secret string) gin.HandlerFunc {
|
||||
Store = memstore.NewStore([]byte(secret))
|
||||
}
|
||||
|
||||
sameSiteMode := http.SameSiteDefaultMode
|
||||
switch strings.ToLower(conf.CORSConfig.SameSite) {
|
||||
case "default":
|
||||
sameSiteMode = http.SameSiteDefaultMode
|
||||
case "none":
|
||||
sameSiteMode = http.SameSiteNoneMode
|
||||
case "strict":
|
||||
sameSiteMode = http.SameSiteStrictMode
|
||||
case "lax":
|
||||
sameSiteMode = http.SameSiteLaxMode
|
||||
}
|
||||
|
||||
// Also set Secure: true if using SSL, you should though
|
||||
// TODO:same-site policy
|
||||
Store.Options(sessions.Options{HttpOnly: true, MaxAge: 60 * 86400, Path: "/"})
|
||||
Store.Options(sessions.Options{
|
||||
HttpOnly: true,
|
||||
MaxAge: 60 * 86400,
|
||||
Path: "/",
|
||||
SameSite: sameSiteMode,
|
||||
Secure: conf.CORSConfig.Secure,
|
||||
})
|
||||
|
||||
return sessions.Sessions("cloudreve-session", Store)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/wopi"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
WopiSessionCtx = "wopi_session"
|
||||
)
|
||||
|
||||
// WopiWriteAccess validates if write access is obtained.
|
||||
func WopiWriteAccess() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := c.MustGet(WopiSessionCtx).(*wopi.SessionCache)
|
||||
if session.Action != wopi.ActionEdit {
|
||||
c.Status(http.StatusNotFound)
|
||||
c.Header(wopi.ServerErrorHeader, "read-only access")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func WopiAccessValidation(w wopi.Client, store cache.Driver) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
accessToken := strings.Split(c.Query(wopi.AccessTokenQuery), ".")
|
||||
if len(accessToken) != 2 {
|
||||
c.Status(http.StatusForbidden)
|
||||
c.Header(wopi.ServerErrorHeader, "malformed access token")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
sessionRaw, exist := store.Get(wopi.SessionCachePrefix + accessToken[0])
|
||||
if !exist {
|
||||
c.Status(http.StatusForbidden)
|
||||
c.Header(wopi.ServerErrorHeader, "invalid access token")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
session := sessionRaw.(wopi.SessionCache)
|
||||
user, err := model.GetActiveUserByID(session.UserID)
|
||||
if err != nil {
|
||||
c.Status(http.StatusInternalServerError)
|
||||
c.Header(wopi.ServerErrorHeader, "user not found")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
fileID := c.MustGet("object_id").(uint)
|
||||
if fileID != session.FileID {
|
||||
c.Status(http.StatusInternalServerError)
|
||||
c.Header(wopi.ServerErrorHeader, "file not found")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("user", &user)
|
||||
c.Set(WopiSessionCtx, &session)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/mocks/wopimock"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/wopi"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWopiWriteAccess(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
rec := httptest.NewRecorder()
|
||||
testFunc := WopiWriteAccess()
|
||||
|
||||
// deny preview only session
|
||||
{
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Set(WopiSessionCtx, &wopi.SessionCache{Action: wopi.ActionPreview})
|
||||
testFunc(c)
|
||||
asserts.True(c.IsAborted())
|
||||
}
|
||||
|
||||
// pass
|
||||
{
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Set(WopiSessionCtx, &wopi.SessionCache{Action: wopi.ActionEdit})
|
||||
testFunc(c)
|
||||
asserts.False(c.IsAborted())
|
||||
}
|
||||
}
|
||||
|
||||
func TestWopiAccessValidation(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
rec := httptest.NewRecorder()
|
||||
mockWopi := &wopimock.WopiClientMock{}
|
||||
mockCache := cache.NewMemoStore()
|
||||
testFunc := WopiAccessValidation(mockWopi, mockCache)
|
||||
|
||||
// malformed access token
|
||||
{
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.AddParam(wopi.AccessTokenQuery, "000")
|
||||
testFunc(c)
|
||||
asserts.True(c.IsAborted())
|
||||
}
|
||||
|
||||
// session key not exist
|
||||
{
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest("GET", "/wopi/files/1?access_token=", nil)
|
||||
query := c.Request.URL.Query()
|
||||
query.Set(wopi.AccessTokenQuery, "sessionID.key")
|
||||
c.Request.URL.RawQuery = query.Encode()
|
||||
testFunc(c)
|
||||
asserts.True(c.IsAborted())
|
||||
}
|
||||
|
||||
// user key not exist
|
||||
{
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest("GET", "/wopi/files/1?access_token=", nil)
|
||||
query := c.Request.URL.Query()
|
||||
query.Set(wopi.AccessTokenQuery, "sessionID.key")
|
||||
c.Request.URL.RawQuery = query.Encode()
|
||||
mockCache.Set(wopi.SessionCachePrefix+"sessionID", wopi.SessionCache{UserID: 1, FileID: 1}, 0)
|
||||
mock.ExpectQuery("SELECT(.+)users(.+)").WillReturnError(errors.New("error"))
|
||||
testFunc(c)
|
||||
asserts.True(c.IsAborted())
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
// file not found
|
||||
{
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest("GET", "/wopi/files/1?access_token=", nil)
|
||||
query := c.Request.URL.Query()
|
||||
query.Set(wopi.AccessTokenQuery, "sessionID.key")
|
||||
c.Request.URL.RawQuery = query.Encode()
|
||||
mockCache.Set(wopi.SessionCachePrefix+"sessionID", wopi.SessionCache{UserID: 1, FileID: 1}, 0)
|
||||
mock.ExpectQuery("SELECT(.+)users(.+)").WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
|
||||
c.Set("object_id", uint(0))
|
||||
testFunc(c)
|
||||
asserts.True(c.IsAborted())
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
// all pass
|
||||
{
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest("GET", "/wopi/files/1?access_token=", nil)
|
||||
query := c.Request.URL.Query()
|
||||
query.Set(wopi.AccessTokenQuery, "sessionID.key")
|
||||
c.Request.URL.RawQuery = query.Encode()
|
||||
mockCache.Set(wopi.SessionCachePrefix+"sessionID", wopi.SessionCache{UserID: 1, FileID: 1}, 0)
|
||||
mock.ExpectQuery("SELECT(.+)users(.+)").WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
|
||||
c.Set("object_id", uint(1))
|
||||
testFunc(c)
|
||||
asserts.False(c.IsAborted())
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
asserts.NotPanics(func() {
|
||||
c.MustGet(WopiSessionCtx)
|
||||
})
|
||||
asserts.NotPanics(func() {
|
||||
c.MustGet("user")
|
||||
})
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -25,7 +25,7 @@ var defaultSettings = []Setting{
|
||||
{Name: "smtpUser", Value: `no-reply@acg.blue`, Type: "mail"},
|
||||
{Name: "smtpPass", Value: ``, Type: "mail"},
|
||||
{Name: "smtpEncryption", Value: `0`, Type: "mail"},
|
||||
{Name: "maxEditSize", Value: `4194304`, Type: "file_edit"},
|
||||
{Name: "maxEditSize", Value: `52428800`, Type: "file_edit"},
|
||||
{Name: "archive_timeout", Value: `600`, Type: "timeout"},
|
||||
{Name: "download_timeout", Value: `600`, Type: "timeout"},
|
||||
{Name: "preview_timeout", Value: `600`, Type: "timeout"},
|
||||
@@ -113,4 +113,10 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti
|
||||
{Name: "pwa_theme_color", Value: "#000000", Type: "pwa"},
|
||||
{Name: "pwa_background_color", Value: "#ffffff", Type: "pwa"},
|
||||
{Name: "office_preview_service", Value: "https://view.officeapps.live.com/op/view.aspx?src={$src}", Type: "preview"},
|
||||
{Name: "show_app_promotion", Value: "1", Type: "mobile"},
|
||||
{Name: "public_resource_maxage", Value: "86400", Type: "timeout"},
|
||||
{Name: "wopi_enabled", Value: "0", Type: "wopi"},
|
||||
{Name: "wopi_endpoint", Value: "", Type: "wopi"},
|
||||
{Name: "wopi_max_size", Value: "52428800", Type: "wopi"},
|
||||
{Name: "wopi_session_timeout", Value: "36000", Type: "wopi"},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
package dialects
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
var keyNameRegex = regexp.MustCompile("[^a-zA-Z0-9]+")
|
||||
|
||||
// DefaultForeignKeyNamer contains the default foreign key name generator method
|
||||
type DefaultForeignKeyNamer struct {
|
||||
}
|
||||
|
||||
type commonDialect struct {
|
||||
db gorm.SQLCommon
|
||||
DefaultForeignKeyNamer
|
||||
}
|
||||
|
||||
func (commonDialect) GetName() string {
|
||||
return "common"
|
||||
}
|
||||
|
||||
func (s *commonDialect) SetDB(db gorm.SQLCommon) {
|
||||
s.db = db
|
||||
}
|
||||
|
||||
func (commonDialect) BindVar(i int) string {
|
||||
return "$$$" // ?
|
||||
}
|
||||
|
||||
func (commonDialect) Quote(key string) string {
|
||||
return fmt.Sprintf(`"%s"`, key)
|
||||
}
|
||||
|
||||
func (s *commonDialect) fieldCanAutoIncrement(field *gorm.StructField) bool {
|
||||
if value, ok := field.TagSettingsGet("AUTO_INCREMENT"); ok {
|
||||
return strings.ToLower(value) != "false"
|
||||
}
|
||||
return field.IsPrimaryKey
|
||||
}
|
||||
|
||||
func (s *commonDialect) DataTypeOf(field *gorm.StructField) string {
|
||||
var dataValue, sqlType, size, additionalType = gorm.ParseFieldStructForDialect(field, s)
|
||||
|
||||
if sqlType == "" {
|
||||
switch dataValue.Kind() {
|
||||
case reflect.Bool:
|
||||
sqlType = "BOOLEAN"
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr:
|
||||
if s.fieldCanAutoIncrement(field) {
|
||||
sqlType = "INTEGER AUTO_INCREMENT"
|
||||
} else {
|
||||
sqlType = "INTEGER"
|
||||
}
|
||||
case reflect.Int64, reflect.Uint64:
|
||||
if s.fieldCanAutoIncrement(field) {
|
||||
sqlType = "BIGINT AUTO_INCREMENT"
|
||||
} else {
|
||||
sqlType = "BIGINT"
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
sqlType = "FLOAT"
|
||||
case reflect.String:
|
||||
if size > 0 && size < 65532 {
|
||||
sqlType = fmt.Sprintf("VARCHAR(%d)", size)
|
||||
} else {
|
||||
sqlType = "VARCHAR(65532)"
|
||||
}
|
||||
case reflect.Struct:
|
||||
if _, ok := dataValue.Interface().(time.Time); ok {
|
||||
sqlType = "TIMESTAMP"
|
||||
}
|
||||
default:
|
||||
if _, ok := dataValue.Interface().([]byte); ok {
|
||||
if size > 0 && size < 65532 {
|
||||
sqlType = fmt.Sprintf("BINARY(%d)", size)
|
||||
} else {
|
||||
sqlType = "BINARY(65532)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sqlType == "" {
|
||||
panic(fmt.Sprintf("invalid sql type %s (%s) for commonDialect", dataValue.Type().Name(), dataValue.Kind().String()))
|
||||
}
|
||||
|
||||
if strings.TrimSpace(additionalType) == "" {
|
||||
return sqlType
|
||||
}
|
||||
return fmt.Sprintf("%v %v", sqlType, additionalType)
|
||||
}
|
||||
|
||||
func currentDatabaseAndTable(dialect gorm.Dialect, tableName string) (string, string) {
|
||||
if strings.Contains(tableName, ".") {
|
||||
splitStrings := strings.SplitN(tableName, ".", 2)
|
||||
return splitStrings[0], splitStrings[1]
|
||||
}
|
||||
return dialect.CurrentDatabase(), tableName
|
||||
}
|
||||
|
||||
func (s commonDialect) HasIndex(tableName string, indexName string) bool {
|
||||
var count int
|
||||
currentDatabase, tableName := currentDatabaseAndTable(&s, tableName)
|
||||
s.db.QueryRow("SELECT count(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE table_schema = ? AND table_name = ? AND index_name = ?", currentDatabase, tableName, indexName).Scan(&count)
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (s commonDialect) RemoveIndex(tableName string, indexName string) error {
|
||||
_, err := s.db.Exec(fmt.Sprintf("DROP INDEX %v", indexName))
|
||||
return err
|
||||
}
|
||||
|
||||
func (s commonDialect) HasForeignKey(tableName string, foreignKeyName string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s commonDialect) HasTable(tableName string) bool {
|
||||
var count int
|
||||
currentDatabase, tableName := currentDatabaseAndTable(&s, tableName)
|
||||
s.db.QueryRow("SELECT count(*) FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = ? AND table_name = ?", currentDatabase, tableName).Scan(&count)
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (s commonDialect) HasColumn(tableName string, columnName string) bool {
|
||||
var count int
|
||||
currentDatabase, tableName := currentDatabaseAndTable(&s, tableName)
|
||||
s.db.QueryRow("SELECT count(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = ? AND table_name = ? AND column_name = ?", currentDatabase, tableName, columnName).Scan(&count)
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (s commonDialect) ModifyColumn(tableName string, columnName string, typ string) error {
|
||||
_, err := s.db.Exec(fmt.Sprintf("ALTER TABLE %v ALTER COLUMN %v TYPE %v", tableName, columnName, typ))
|
||||
return err
|
||||
}
|
||||
|
||||
func (s commonDialect) CurrentDatabase() (name string) {
|
||||
s.db.QueryRow("SELECT DATABASE()").Scan(&name)
|
||||
return
|
||||
}
|
||||
|
||||
func (commonDialect) LimitAndOffsetSQL(limit, offset interface{}) (sql string) {
|
||||
if limit != nil {
|
||||
if parsedLimit, err := strconv.ParseInt(fmt.Sprint(limit), 0, 0); err == nil && parsedLimit >= 0 {
|
||||
sql += fmt.Sprintf(" LIMIT %d", parsedLimit)
|
||||
}
|
||||
}
|
||||
if offset != nil {
|
||||
if parsedOffset, err := strconv.ParseInt(fmt.Sprint(offset), 0, 0); err == nil && parsedOffset >= 0 {
|
||||
sql += fmt.Sprintf(" OFFSET %d", parsedOffset)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (commonDialect) SelectFromDummyTable() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (commonDialect) LastInsertIDReturningSuffix(tableName, columnName string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (commonDialect) DefaultValueStr() string {
|
||||
return "DEFAULT VALUES"
|
||||
}
|
||||
|
||||
// BuildKeyName returns a valid key name (foreign key, index key) for the given table, field and reference
|
||||
func (DefaultForeignKeyNamer) BuildKeyName(kind, tableName string, fields ...string) string {
|
||||
keyName := fmt.Sprintf("%s_%s_%s", kind, tableName, strings.Join(fields, "_"))
|
||||
keyName = keyNameRegex.ReplaceAllString(keyName, "_")
|
||||
return keyName
|
||||
}
|
||||
|
||||
// NormalizeIndexAndColumn returns argument's index name and column name without doing anything
|
||||
func (commonDialect) NormalizeIndexAndColumn(indexName, columnName string) (string, string) {
|
||||
return indexName, columnName
|
||||
}
|
||||
|
||||
// IsByteArrayOrSlice returns true of the reflected value is an array or slice
|
||||
func IsByteArrayOrSlice(value reflect.Value) bool {
|
||||
return (value.Kind() == reflect.Array || value.Kind() == reflect.Slice) && value.Type().Elem() == reflect.TypeOf(uint8(0))
|
||||
}
|
||||
|
||||
type sqlite struct {
|
||||
commonDialect
|
||||
}
|
||||
|
||||
func init() {
|
||||
gorm.RegisterDialect("sqlite", &sqlite{})
|
||||
}
|
||||
|
||||
func (sqlite) GetName() string {
|
||||
return "sqlite"
|
||||
}
|
||||
|
||||
// Get Data Type for Sqlite Dialect
|
||||
func (s *sqlite) DataTypeOf(field *gorm.StructField) string {
|
||||
var dataValue, sqlType, size, additionalType = gorm.ParseFieldStructForDialect(field, s)
|
||||
|
||||
if sqlType == "" {
|
||||
switch dataValue.Kind() {
|
||||
case reflect.Bool:
|
||||
sqlType = "bool"
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr:
|
||||
if s.fieldCanAutoIncrement(field) {
|
||||
field.TagSettingsSet("AUTO_INCREMENT", "AUTO_INCREMENT")
|
||||
sqlType = "integer primary key autoincrement"
|
||||
} else {
|
||||
sqlType = "integer"
|
||||
}
|
||||
case reflect.Int64, reflect.Uint64:
|
||||
if s.fieldCanAutoIncrement(field) {
|
||||
field.TagSettingsSet("AUTO_INCREMENT", "AUTO_INCREMENT")
|
||||
sqlType = "integer primary key autoincrement"
|
||||
} else {
|
||||
sqlType = "bigint"
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
sqlType = "real"
|
||||
case reflect.String:
|
||||
if size > 0 && size < 65532 {
|
||||
sqlType = fmt.Sprintf("varchar(%d)", size)
|
||||
} else {
|
||||
sqlType = "text"
|
||||
}
|
||||
case reflect.Struct:
|
||||
if _, ok := dataValue.Interface().(time.Time); ok {
|
||||
sqlType = "datetime"
|
||||
}
|
||||
default:
|
||||
if IsByteArrayOrSlice(dataValue) {
|
||||
sqlType = "blob"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sqlType == "" {
|
||||
panic(fmt.Sprintf("invalid sql type %s (%s) for sqlite", dataValue.Type().Name(), dataValue.Kind().String()))
|
||||
}
|
||||
|
||||
if strings.TrimSpace(additionalType) == "" {
|
||||
return sqlType
|
||||
}
|
||||
return fmt.Sprintf("%v %v", sqlType, additionalType)
|
||||
}
|
||||
|
||||
func (s sqlite) HasIndex(tableName string, indexName string) bool {
|
||||
var count int
|
||||
s.db.QueryRow(fmt.Sprintf("SELECT count(*) FROM sqlite_master WHERE tbl_name = ? AND sql LIKE '%%INDEX %v ON%%'", indexName), tableName).Scan(&count)
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (s sqlite) HasTable(tableName string) bool {
|
||||
var count int
|
||||
s.db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?", tableName).Scan(&count)
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (s sqlite) HasColumn(tableName string, columnName string) bool {
|
||||
var count int
|
||||
s.db.QueryRow(fmt.Sprintf("SELECT count(*) FROM sqlite_master WHERE tbl_name = ? AND (sql LIKE '%%\"%v\" %%' OR sql LIKE '%%%v %%');", columnName, columnName), tableName).Scan(&count)
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (s sqlite) CurrentDatabase() (name string) {
|
||||
var (
|
||||
ifaces = make([]interface{}, 3)
|
||||
pointers = make([]*string, 3)
|
||||
i int
|
||||
)
|
||||
for i = 0; i < 3; i++ {
|
||||
ifaces[i] = &pointers[i]
|
||||
}
|
||||
if err := s.db.QueryRow("PRAGMA database_list").Scan(ifaces...); err != nil {
|
||||
return
|
||||
}
|
||||
if pointers[1] != nil {
|
||||
name = *pointers[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
+6
-4
@@ -234,7 +234,7 @@ func DeleteFiles(files []*File, uid uint) error {
|
||||
user.ID = uid
|
||||
var size uint64
|
||||
for _, file := range files {
|
||||
if file.UserID != uid {
|
||||
if uid > 0 && file.UserID != uid {
|
||||
tx.Rollback()
|
||||
return errors.New("user id not consistent")
|
||||
}
|
||||
@@ -253,9 +253,11 @@ func DeleteFiles(files []*File, uid uint) error {
|
||||
size += file.Size
|
||||
}
|
||||
|
||||
if err := user.ChangeStorage(tx, "-", size); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
if uid > 0 {
|
||||
if err := user.ChangeStorage(tx, "-", size); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
|
||||
+19
-6
@@ -365,7 +365,7 @@ func TestDeleteFiles(t *testing.T) {
|
||||
|
||||
// uid 不一致
|
||||
{
|
||||
err := DeleteFiles([]*File{{}}, 1)
|
||||
err := DeleteFiles([]*File{{UserID: 2}}, 1)
|
||||
a.Contains("user id not consistent", err.Error())
|
||||
}
|
||||
|
||||
@@ -375,7 +375,7 @@ func TestDeleteFiles(t *testing.T) {
|
||||
mock.ExpectExec("DELETE(.+)").
|
||||
WillReturnError(errors.New("error"))
|
||||
mock.ExpectRollback()
|
||||
err := DeleteFiles([]*File{{}}, 0)
|
||||
err := DeleteFiles([]*File{{UserID: 1}}, 1)
|
||||
a.NoError(mock.ExpectationsWereMet())
|
||||
a.Error(err)
|
||||
}
|
||||
@@ -387,7 +387,7 @@ func TestDeleteFiles(t *testing.T) {
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectExec("UPDATE(.+)storage(.+)").WillReturnError(errors.New("error"))
|
||||
mock.ExpectRollback()
|
||||
err := DeleteFiles([]*File{{}}, 0)
|
||||
err := DeleteFiles([]*File{{UserID: 1}}, 1)
|
||||
a.NoError(mock.ExpectationsWereMet())
|
||||
a.Error(err)
|
||||
}
|
||||
@@ -398,7 +398,7 @@ func TestDeleteFiles(t *testing.T) {
|
||||
mock.ExpectExec("DELETE(.+)").
|
||||
WillReturnResult(sqlmock.NewResult(1, 0))
|
||||
mock.ExpectRollback()
|
||||
err := DeleteFiles([]*File{{Size: 1}, {Size: 2}}, 0)
|
||||
err := DeleteFiles([]*File{{Size: 1, UserID: 1}, {Size: 2, UserID: 1}}, 1)
|
||||
a.NoError(mock.ExpectationsWereMet())
|
||||
a.Error(err)
|
||||
a.Contains("file size is dirty", err.Error())
|
||||
@@ -411,9 +411,22 @@ func TestDeleteFiles(t *testing.T) {
|
||||
WillReturnResult(sqlmock.NewResult(2, 1))
|
||||
mock.ExpectExec("DELETE(.+)").
|
||||
WillReturnResult(sqlmock.NewResult(2, 1))
|
||||
mock.ExpectExec("UPDATE(.+)storage(.+)").WithArgs(uint64(3), sqlmock.AnyArg()).WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectExec("UPDATE(.+)storage(.+)").WithArgs(uint64(3), sqlmock.AnyArg(), uint(1)).WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
err := DeleteFiles([]*File{{Size: 1}, {Size: 2}}, 0)
|
||||
err := DeleteFiles([]*File{{Size: 1, UserID: 1}, {Size: 2, UserID: 1}}, 1)
|
||||
a.NoError(mock.ExpectationsWereMet())
|
||||
a.NoError(err)
|
||||
}
|
||||
|
||||
// 成功, 关联用户不存在
|
||||
{
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("DELETE(.+)").
|
||||
WillReturnResult(sqlmock.NewResult(2, 1))
|
||||
mock.ExpectExec("DELETE(.+)").
|
||||
WillReturnResult(sqlmock.NewResult(2, 1))
|
||||
mock.ExpectCommit()
|
||||
err := DeleteFiles([]*File{{Size: 1, UserID: 1}, {Size: 2, UserID: 1}}, 0)
|
||||
a.NoError(mock.ExpectationsWereMet())
|
||||
a.NoError(err)
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ func TestFolder_GetChildFolder(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetRecursiveChildFolderSQLite(t *testing.T) {
|
||||
conf.DatabaseConfig.Type = "sqlite3"
|
||||
conf.DatabaseConfig.Type = "sqlite"
|
||||
asserts := assert.New(t)
|
||||
|
||||
// 测试目录结构
|
||||
|
||||
@@ -34,6 +34,7 @@ type GroupOption struct {
|
||||
SourceBatchSize int `json:"source_batch,omitempty"`
|
||||
RedirectedSource bool `json:"redirected_source,omitempty"`
|
||||
Aria2BatchSize int `json:"aria2_batch,omitempty"`
|
||||
AdvanceDelete bool `json:"advance_delete,omitempty"`
|
||||
}
|
||||
|
||||
// GetGroupByID 用ID获取用户组
|
||||
|
||||
+30
-14
@@ -9,10 +9,11 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
_ "github.com/cloudreve/Cloudreve/v3/models/dialects"
|
||||
_ "github.com/glebarez/go-sqlite"
|
||||
_ "github.com/jinzhu/gorm/dialects/mssql"
|
||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
)
|
||||
|
||||
// DB 数据库链接单例
|
||||
@@ -23,35 +24,50 @@ func Init() {
|
||||
util.Log().Info("Initializing database connection...")
|
||||
|
||||
var (
|
||||
db *gorm.DB
|
||||
err error
|
||||
db *gorm.DB
|
||||
err error
|
||||
confDBType string = conf.DatabaseConfig.Type
|
||||
)
|
||||
|
||||
// 兼容已有配置中的 "sqlite3" 配置项
|
||||
if confDBType == "sqlite3" {
|
||||
confDBType = "sqlite"
|
||||
}
|
||||
|
||||
if gin.Mode() == gin.TestMode {
|
||||
// 测试模式下,使用内存数据库
|
||||
db, err = gorm.Open("sqlite3", ":memory:")
|
||||
db, err = gorm.Open("sqlite", ":memory:")
|
||||
} else {
|
||||
switch conf.DatabaseConfig.Type {
|
||||
case "UNSET", "sqlite", "sqlite3":
|
||||
// 未指定数据库或者明确指定为 sqlite 时,使用 SQLite3 数据库
|
||||
db, err = gorm.Open("sqlite3", util.RelativePath(conf.DatabaseConfig.DBFile))
|
||||
switch confDBType {
|
||||
case "UNSET", "sqlite":
|
||||
// 未指定数据库或者明确指定为 sqlite 时,使用 SQLite 数据库
|
||||
db, err = gorm.Open("sqlite", util.RelativePath(conf.DatabaseConfig.DBFile))
|
||||
case "postgres":
|
||||
db, err = gorm.Open(conf.DatabaseConfig.Type, fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable",
|
||||
db, err = gorm.Open(confDBType, fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable",
|
||||
conf.DatabaseConfig.Host,
|
||||
conf.DatabaseConfig.User,
|
||||
conf.DatabaseConfig.Password,
|
||||
conf.DatabaseConfig.Name,
|
||||
conf.DatabaseConfig.Port))
|
||||
case "mysql", "mssql":
|
||||
db, err = gorm.Open(conf.DatabaseConfig.Type, fmt.Sprintf("%s:%s@(%s:%d)/%s?charset=%s&parseTime=True&loc=Local",
|
||||
var host string
|
||||
if conf.DatabaseConfig.UnixSocket {
|
||||
host = fmt.Sprintf("unix(%s)",
|
||||
conf.DatabaseConfig.Host)
|
||||
} else {
|
||||
host = fmt.Sprintf("(%s:%d)",
|
||||
conf.DatabaseConfig.Host,
|
||||
conf.DatabaseConfig.Port)
|
||||
}
|
||||
|
||||
db, err = gorm.Open(confDBType, fmt.Sprintf("%s:%s@%s/%s?charset=%s&parseTime=True&loc=Local",
|
||||
conf.DatabaseConfig.User,
|
||||
conf.DatabaseConfig.Password,
|
||||
conf.DatabaseConfig.Host,
|
||||
conf.DatabaseConfig.Port,
|
||||
host,
|
||||
conf.DatabaseConfig.Name,
|
||||
conf.DatabaseConfig.Charset))
|
||||
default:
|
||||
util.Log().Panic("Unsupported database type %q.", conf.DatabaseConfig.Type)
|
||||
util.Log().Panic("Unsupported database type %q.", confDBType)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +90,7 @@ func Init() {
|
||||
|
||||
//设置连接池
|
||||
db.DB().SetMaxIdleConns(50)
|
||||
if conf.DatabaseConfig.Type == "sqlite" || conf.DatabaseConfig.Type == "sqlite3" || conf.DatabaseConfig.Type == "UNSET" {
|
||||
if confDBType == "sqlite" || confDBType == "UNSET" {
|
||||
db.DB().SetMaxOpenConns(1)
|
||||
} else {
|
||||
db.DB().SetMaxOpenConns(100)
|
||||
|
||||
@@ -111,6 +111,7 @@ func addDefaultGroups() {
|
||||
SourceBatchSize: 1000,
|
||||
Aria2BatchSize: 50,
|
||||
RedirectedSource: true,
|
||||
AdvanceDelete: true,
|
||||
},
|
||||
}
|
||||
if err := DB.Create(&defaultAdminGroup).Error; err != nil {
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
|
||||
func TestMigration(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
conf.DatabaseConfig.Type = "sqlite3"
|
||||
DB, _ = gorm.Open("sqlite3", ":memory:")
|
||||
conf.DatabaseConfig.Type = "sqlite"
|
||||
DB, _ = gorm.Open("sqlite", ":memory:")
|
||||
|
||||
asserts.NotPanics(func() {
|
||||
migration()
|
||||
|
||||
+4
-1
@@ -65,6 +65,9 @@ type PolicyOption struct {
|
||||
TPSLimit float64 `json:"tps_limit,omitempty"`
|
||||
// 每秒 API 请求爆发上限
|
||||
TPSLimitBurst int `json:"tps_limit_burst,omitempty"`
|
||||
// Set this to `true` to force the request to use path-style addressing,
|
||||
// i.e., `http://s3.amazonaws.com/BUCKET/KEY `
|
||||
S3ForcePathStyle bool `json:"s3_path_style"`
|
||||
}
|
||||
|
||||
// thumbSuffix 支持缩略图处理的文件扩展名
|
||||
@@ -122,7 +125,7 @@ func (policy *Policy) BeforeSave() (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
//SerializeOptions 将序列后的Option写入到数据库字段
|
||||
// SerializeOptions 将序列后的Option写入到数据库字段
|
||||
func (policy *Policy) SerializeOptions() (err error) {
|
||||
optionsValue, err := json.Marshal(&policy.OptionsSerialized)
|
||||
policy.Options = string(optionsValue)
|
||||
|
||||
@@ -11,6 +11,7 @@ type Webdav struct {
|
||||
Password string `gorm:"unique_index:password_only_on"` // 应用密码
|
||||
UserID uint `gorm:"unique_index:password_only_on"` // 用户ID
|
||||
Root string `gorm:"type:text"` // 根目录
|
||||
Readonly bool `gorm:"type:bool"` // 是否只读
|
||||
}
|
||||
|
||||
// Create 创建账户
|
||||
@@ -39,3 +40,8 @@ func ListWebDAVAccounts(uid uint) []Webdav {
|
||||
func DeleteWebDAVAccountByID(id, uid uint) {
|
||||
DB.Where("user_id = ? and id = ?", uid, id).Delete(&Webdav{})
|
||||
}
|
||||
|
||||
// UpdateWebDAVAccountReadonlyByID 根据账户ID和UID更新账户的只读性
|
||||
func UpdateWebDAVAccountReadonlyByID(id, uid uint, readonly bool) {
|
||||
DB.Model(&Webdav{Model: gorm.Model{ID: id}, UserID: uid}).UpdateColumn("readonly", readonly)
|
||||
}
|
||||
|
||||
+7
-3
@@ -17,6 +17,7 @@ type database struct {
|
||||
DBFile string
|
||||
Port int
|
||||
Charset string
|
||||
UnixSocket bool
|
||||
}
|
||||
|
||||
// system 系统通用配置
|
||||
@@ -26,7 +27,8 @@ type system struct {
|
||||
Debug bool
|
||||
SessionSecret string
|
||||
HashIDSalt string
|
||||
GracePeriod int `validate:"gte=0"`
|
||||
GracePeriod int `validate:"gte=0"`
|
||||
ProxyHeader string `validate:"required_with=Listen"`
|
||||
}
|
||||
|
||||
type ssl struct {
|
||||
@@ -36,8 +38,8 @@ type ssl struct {
|
||||
}
|
||||
|
||||
type unix struct {
|
||||
Listen string
|
||||
ProxyHeader string `validate:"required_with=Listen"`
|
||||
Listen string
|
||||
Perm uint32
|
||||
}
|
||||
|
||||
// slave 作为slave存储端配置
|
||||
@@ -62,6 +64,8 @@ type cors struct {
|
||||
AllowHeaders []string
|
||||
AllowCredentials bool
|
||||
ExposeHeaders []string
|
||||
SameSite string
|
||||
Secure bool
|
||||
}
|
||||
|
||||
var cfg *ini.File
|
||||
|
||||
+12
-9
@@ -10,17 +10,19 @@ var RedisConfig = &redis{
|
||||
|
||||
// DatabaseConfig 数据库配置
|
||||
var DatabaseConfig = &database{
|
||||
Type: "UNSET",
|
||||
Charset: "utf8",
|
||||
DBFile: "cloudreve.db",
|
||||
Port: 3306,
|
||||
Type: "UNSET",
|
||||
Charset: "utf8",
|
||||
DBFile: "cloudreve.db",
|
||||
Port: 3306,
|
||||
UnixSocket: false,
|
||||
}
|
||||
|
||||
// SystemConfig 系统公用配置
|
||||
var SystemConfig = &system{
|
||||
Debug: false,
|
||||
Mode: "master",
|
||||
Listen: ":5212",
|
||||
Debug: false,
|
||||
Mode: "master",
|
||||
Listen: ":5212",
|
||||
ProxyHeader: "X-Forwarded-For",
|
||||
}
|
||||
|
||||
// CORSConfig 跨域配置
|
||||
@@ -30,6 +32,8 @@ var CORSConfig = &cors{
|
||||
AllowHeaders: []string{"Cookie", "X-Cr-Policy", "Authorization", "Content-Length", "Content-Type", "X-Cr-Path", "X-Cr-FileName"},
|
||||
AllowCredentials: false,
|
||||
ExposeHeaders: nil,
|
||||
SameSite: "Default",
|
||||
Secure: false,
|
||||
}
|
||||
|
||||
// SlaveConfig 从机配置
|
||||
@@ -45,8 +49,7 @@ var SSLConfig = &ssl{
|
||||
}
|
||||
|
||||
var UnixConfig = &unix{
|
||||
Listen: "",
|
||||
ProxyHeader: "X-Forwarded-For",
|
||||
Listen: "",
|
||||
}
|
||||
|
||||
var OptionOverwrite = map[string]interface{}{}
|
||||
|
||||
+3
-3
@@ -1,13 +1,13 @@
|
||||
package conf
|
||||
|
||||
// BackendVersion 当前后端版本号
|
||||
var BackendVersion = "3.6.0-beta1"
|
||||
var BackendVersion = "3.7.1"
|
||||
|
||||
// RequiredDBVersion 与当前版本匹配的数据库版本
|
||||
var RequiredDBVersion = "3.6.0-beta1"
|
||||
var RequiredDBVersion = "3.7.1"
|
||||
|
||||
// RequiredStaticVersion 与当前版本匹配的静态资源版本
|
||||
var RequiredStaticVersion = "3.6.0-beta1"
|
||||
var RequiredStaticVersion = "3.7.1"
|
||||
|
||||
// IsPro 是否为Pro版本
|
||||
var IsPro = "false"
|
||||
|
||||
@@ -88,7 +88,7 @@ func uploadSessionCollect() {
|
||||
continue
|
||||
}
|
||||
|
||||
if err = fs.Delete(context.Background(), []uint{}, filesIDs, false); err != nil {
|
||||
if err = fs.Delete(context.Background(), []uint{}, filesIDs, false, false); err != nil {
|
||||
util.Log().Warning("Failed to delete upload session: %s", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ type UploadPolicy struct {
|
||||
Conditions []interface{} `json:"conditions"`
|
||||
}
|
||||
|
||||
//MetaData 文件信息
|
||||
// MetaData 文件信息
|
||||
type MetaData struct {
|
||||
Size uint64
|
||||
Etag string
|
||||
@@ -71,7 +71,7 @@ func (handler *Driver) InitS3Client() error {
|
||||
Credentials: credentials.NewStaticCredentials(handler.Policy.AccessKey, handler.Policy.SecretKey, ""),
|
||||
Endpoint: &handler.Policy.Server,
|
||||
Region: &handler.Policy.OptionsSerialized.Region,
|
||||
S3ForcePathStyle: aws.Bool(true),
|
||||
S3ForcePathStyle: &handler.Policy.OptionsSerialized.S3ForcePathStyle,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -289,17 +289,17 @@ func (handler *Driver) Source(
|
||||
return "", err
|
||||
}
|
||||
|
||||
contentDescription := aws.String("attachment; filename=\"" + url.PathEscape(fileName) + "\"")
|
||||
if !isDownload {
|
||||
contentDescription = nil
|
||||
}
|
||||
req, _ := handler.svc.GetObjectRequest(
|
||||
&s3.GetObjectInput{
|
||||
Bucket: &handler.Policy.BucketName,
|
||||
Key: &path,
|
||||
ResponseContentDisposition: aws.String("attachment; filename=\"" + url.PathEscape(fileName) + "\""),
|
||||
ResponseContentDisposition: contentDescription,
|
||||
})
|
||||
|
||||
if ttl == 0 {
|
||||
ttl = 3600
|
||||
}
|
||||
|
||||
signedURL, err := req.Presign(time.Duration(ttl) * time.Second)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -398,8 +398,8 @@ func (handler *Driver) Token(ctx context.Context, ttl int64, uploadSession *seri
|
||||
|
||||
// Meta 获取文件信息
|
||||
func (handler *Driver) Meta(ctx context.Context, path string) (*MetaData, error) {
|
||||
res, err := handler.svc.GetObject(
|
||||
&s3.GetObjectInput{
|
||||
res, err := handler.svc.HeadObject(
|
||||
&s3.HeadObjectInput{
|
||||
Bucket: &handler.Policy.BucketName,
|
||||
Key: &path,
|
||||
})
|
||||
@@ -407,7 +407,6 @@ func (handler *Driver) Meta(ctx context.Context, path string) (*MetaData, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
return &MetaData{
|
||||
Size: uint64(*res.ContentLength),
|
||||
|
||||
@@ -120,8 +120,9 @@ func (fs *FileSystem) Move(ctx context.Context, dirs, files []uint, src, dst str
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete 递归删除对象, force 为 true 时强制删除文件记录,忽略物理删除是否成功
|
||||
func (fs *FileSystem) Delete(ctx context.Context, dirs, files []uint, force bool) error {
|
||||
// Delete 递归删除对象, force 为 true 时强制删除文件记录,忽略物理删除是否成功;
|
||||
// unlink 为 true 时只删除虚拟文件系统的文件记录,不删除物理文件。
|
||||
func (fs *FileSystem) Delete(ctx context.Context, dirs, files []uint, force, unlink bool) error {
|
||||
// 已删除的文件ID
|
||||
var deletedFiles = make([]*model.File, 0, len(fs.FileTarget))
|
||||
// 删除失败的文件的父目录ID
|
||||
@@ -155,7 +156,10 @@ func (fs *FileSystem) Delete(ctx context.Context, dirs, files []uint, force bool
|
||||
policyGroup := fs.GroupFileByPolicy(ctx, filesToBeDelete)
|
||||
|
||||
// 按照存储策略分组删除对象
|
||||
failed := fs.deleteGroupedFile(ctx, policyGroup)
|
||||
failed := make(map[uint][]string)
|
||||
if !unlink {
|
||||
failed = fs.deleteGroupedFile(ctx, policyGroup)
|
||||
}
|
||||
|
||||
// 整理删除结果
|
||||
for i := 0; i < len(fs.FileTarget); i++ {
|
||||
|
||||
@@ -3,13 +3,13 @@ package filesystem
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
|
||||
testMock "github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
|
||||
@@ -485,7 +485,6 @@ func TestFileSystem_Delete(t *testing.T) {
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectExec("DELETE(.+)").
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectExec("UPDATE(.+)users(.+)storage(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
// 删除对应分享
|
||||
mock.ExpectBegin()
|
||||
@@ -505,7 +504,7 @@ func TestFileSystem_Delete(t *testing.T) {
|
||||
|
||||
fs.FileTarget = []model.File{}
|
||||
fs.DirTarget = []model.Folder{}
|
||||
err := fs.Delete(ctx, []uint{1}, []uint{1}, true)
|
||||
err := fs.Delete(ctx, []uint{1}, []uint{1}, true, false)
|
||||
asserts.NoError(err)
|
||||
}
|
||||
//全部成功
|
||||
@@ -543,7 +542,6 @@ func TestFileSystem_Delete(t *testing.T) {
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectExec("DELETE(.+)").
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectExec("UPDATE(.+)users(.+)storage(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
// 删除对应分享
|
||||
mock.ExpectBegin()
|
||||
@@ -563,7 +561,7 @@ func TestFileSystem_Delete(t *testing.T) {
|
||||
|
||||
fs.FileTarget = []model.File{}
|
||||
fs.DirTarget = []model.Folder{}
|
||||
err = fs.Delete(ctx, []uint{1}, []uint{1}, false)
|
||||
err = fs.Delete(ctx, []uint{1}, []uint{1}, false, false)
|
||||
asserts.NoError(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package cachemock
|
||||
|
||||
import "github.com/stretchr/testify/mock"
|
||||
|
||||
type CacheClientMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (c CacheClientMock) Set(key string, value interface{}, ttl int) error {
|
||||
return c.Called(key, value, ttl).Error(0)
|
||||
}
|
||||
|
||||
func (c CacheClientMock) Get(key string) (interface{}, bool) {
|
||||
args := c.Called(key)
|
||||
return args.Get(0), args.Bool(1)
|
||||
}
|
||||
|
||||
func (c CacheClientMock) Gets(keys []string, prefix string) (map[string]interface{}, []string) {
|
||||
args := c.Called(keys, prefix)
|
||||
return args.Get(0).(map[string]interface{}), args.Get(1).([]string)
|
||||
}
|
||||
|
||||
func (c CacheClientMock) Sets(values map[string]interface{}, prefix string) error {
|
||||
return c.Called(values).Error(0)
|
||||
}
|
||||
|
||||
func (c CacheClientMock) Delete(keys []string, prefix string) error {
|
||||
return c.Called(keys, prefix).Error(0)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package wopimock
|
||||
|
||||
import (
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/wopi"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type WopiClientMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (w *WopiClientMock) NewSession(user *model.User, file *model.File, action wopi.ActonType) (*wopi.Session, error) {
|
||||
args := w.Called(user, file, action)
|
||||
return args.Get(0).(*wopi.Session), args.Error(1)
|
||||
}
|
||||
|
||||
func (w *WopiClientMock) AvailableExts() []string {
|
||||
args := w.Called()
|
||||
return args.Get(0).([]string)
|
||||
}
|
||||
@@ -84,3 +84,49 @@ type Sources struct {
|
||||
Parent uint `json:"parent"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// DocPreviewSession 文档预览会话响应
|
||||
type DocPreviewSession struct {
|
||||
URL string `json:"url"`
|
||||
AccessToken string `json:"access_token,omitempty"`
|
||||
AccessTokenTTL int64 `json:"access_token_ttl,omitempty"`
|
||||
}
|
||||
|
||||
// WopiFileInfo Response for `CheckFileInfo`
|
||||
type WopiFileInfo struct {
|
||||
// Required
|
||||
BaseFileName string
|
||||
Version string
|
||||
Size int64
|
||||
|
||||
// Breadcrumb
|
||||
BreadcrumbBrandName string
|
||||
BreadcrumbBrandUrl string
|
||||
BreadcrumbFolderName string
|
||||
BreadcrumbFolderUrl string
|
||||
|
||||
// Post Message
|
||||
FileSharingPostMessage bool
|
||||
ClosePostMessage bool
|
||||
PostMessageOrigin string
|
||||
|
||||
// Other miscellaneous properties
|
||||
FileNameMaxLength int
|
||||
LastModifiedTime string
|
||||
|
||||
// User metadata
|
||||
IsAnonymousUser bool
|
||||
UserFriendlyName string
|
||||
UserId string
|
||||
OwnerId string
|
||||
|
||||
// Permission
|
||||
ReadOnly bool
|
||||
UserCanRename bool
|
||||
UserCanReview bool
|
||||
UserCanWrite bool
|
||||
|
||||
SupportsRename bool
|
||||
SupportsReviewing bool
|
||||
SupportsUpdate bool
|
||||
}
|
||||
|
||||
+20
-16
@@ -7,21 +7,23 @@ import (
|
||||
|
||||
// SiteConfig 站点全局设置序列
|
||||
type SiteConfig struct {
|
||||
SiteName string `json:"title"`
|
||||
LoginCaptcha bool `json:"loginCaptcha"`
|
||||
RegCaptcha bool `json:"regCaptcha"`
|
||||
ForgetCaptcha bool `json:"forgetCaptcha"`
|
||||
EmailActive bool `json:"emailActive"`
|
||||
Themes string `json:"themes"`
|
||||
DefaultTheme string `json:"defaultTheme"`
|
||||
HomepageViewMethod string `json:"home_view_method"`
|
||||
ShareViewMethod string `json:"share_view_method"`
|
||||
Authn bool `json:"authn"`
|
||||
User User `json:"user"`
|
||||
ReCaptchaKey string `json:"captcha_ReCaptchaKey"`
|
||||
CaptchaType string `json:"captcha_type"`
|
||||
TCaptchaCaptchaAppId string `json:"tcaptcha_captcha_app_id"`
|
||||
RegisterEnabled bool `json:"registerEnabled"`
|
||||
SiteName string `json:"title"`
|
||||
LoginCaptcha bool `json:"loginCaptcha"`
|
||||
RegCaptcha bool `json:"regCaptcha"`
|
||||
ForgetCaptcha bool `json:"forgetCaptcha"`
|
||||
EmailActive bool `json:"emailActive"`
|
||||
Themes string `json:"themes"`
|
||||
DefaultTheme string `json:"defaultTheme"`
|
||||
HomepageViewMethod string `json:"home_view_method"`
|
||||
ShareViewMethod string `json:"share_view_method"`
|
||||
Authn bool `json:"authn"`
|
||||
User User `json:"user"`
|
||||
ReCaptchaKey string `json:"captcha_ReCaptchaKey"`
|
||||
CaptchaType string `json:"captcha_type"`
|
||||
TCaptchaCaptchaAppId string `json:"tcaptcha_captcha_app_id"`
|
||||
RegisterEnabled bool `json:"registerEnabled"`
|
||||
AppPromotion bool `json:"app_promotion"`
|
||||
WopiExts []string `json:"wopi_exts"`
|
||||
}
|
||||
|
||||
type task struct {
|
||||
@@ -59,7 +61,7 @@ func checkSettingValue(setting map[string]string, key string) string {
|
||||
}
|
||||
|
||||
// BuildSiteConfig 站点全局设置
|
||||
func BuildSiteConfig(settings map[string]string, user *model.User) Response {
|
||||
func BuildSiteConfig(settings map[string]string, user *model.User, wopiExts []string) Response {
|
||||
var userRes User
|
||||
if user != nil {
|
||||
userRes = BuildUser(*user)
|
||||
@@ -83,6 +85,8 @@ func BuildSiteConfig(settings map[string]string, user *model.User) Response {
|
||||
CaptchaType: checkSettingValue(settings, "captcha_type"),
|
||||
TCaptchaCaptchaAppId: checkSettingValue(settings, "captcha_TCaptcha_CaptchaAppId"),
|
||||
RegisterEnabled: model.IsTrueVal(checkSettingValue(settings, "register_enabled")),
|
||||
AppPromotion: model.IsTrueVal(checkSettingValue(settings, "show_app_promotion")),
|
||||
WopiExts: wopiExts,
|
||||
}}
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@ func TestCheckSettingValue(t *testing.T) {
|
||||
func TestBuildSiteConfig(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
|
||||
res := BuildSiteConfig(map[string]string{"not exist": ""}, &model.User{})
|
||||
res := BuildSiteConfig(map[string]string{"not exist": ""}, &model.User{}, nil)
|
||||
asserts.Equal("", res.Data.(SiteConfig).SiteName)
|
||||
|
||||
res = BuildSiteConfig(map[string]string{"siteName": "123"}, &model.User{})
|
||||
res = BuildSiteConfig(map[string]string{"siteName": "123"}, &model.User{}, nil)
|
||||
asserts.Equal("123", res.Data.(SiteConfig).SiteName)
|
||||
|
||||
// 非空用户
|
||||
@@ -29,7 +29,7 @@ func TestBuildSiteConfig(t *testing.T) {
|
||||
Model: gorm.Model{
|
||||
ID: 5,
|
||||
},
|
||||
})
|
||||
}, nil)
|
||||
asserts.Len(res.Data.(SiteConfig).User.ID, 4)
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ type group struct {
|
||||
CompressEnabled bool `json:"compress"`
|
||||
WebDAVEnabled bool `json:"webdav"`
|
||||
SourceBatchSize int `json:"sourceBatch"`
|
||||
AdvanceDelete bool `json:"advanceDelete"`
|
||||
}
|
||||
|
||||
type tag struct {
|
||||
@@ -100,6 +101,7 @@ func BuildUser(user model.User) User {
|
||||
CompressEnabled: user.Group.OptionsSerialized.ArchiveTask,
|
||||
WebDAVEnabled: user.Group.WebDAVEnabled,
|
||||
SourceBatchSize: user.Group.OptionsSerialized.SourceBatchSize,
|
||||
AdvanceDelete: user.Group.OptionsSerialized.AdvanceDelete,
|
||||
},
|
||||
Tags: buildTagRes(tags),
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestBuildRegexp(t *testing.T) {
|
||||
func TestBuildConcat(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
asserts.Equal("CONCAT(1,2)", BuildConcat("1", "2", "mysql"))
|
||||
asserts.Equal("1||2", BuildConcat("1", "2", "sqlite3"))
|
||||
asserts.Equal("1||2", BuildConcat("1", "2", "sqlite"))
|
||||
}
|
||||
|
||||
func TestSliceDifference(t *testing.T) {
|
||||
|
||||
@@ -218,7 +218,7 @@ func (h *Handler) confirmLocks(r *http.Request, src, dst string, fs *filesystem.
|
||||
}, 0, nil
|
||||
}
|
||||
|
||||
//OK
|
||||
// OK
|
||||
func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request, fs *filesystem.FileSystem) (status int, err error) {
|
||||
reqPath, status, err := h.stripPrefix(r.URL.Path, fs.User.ID)
|
||||
if err != nil {
|
||||
@@ -303,7 +303,7 @@ func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request, fs *files
|
||||
|
||||
// 尝试作为文件删除
|
||||
if ok, file := fs.IsFileExist(reqPath); ok {
|
||||
if err := fs.Delete(ctx, []uint{}, []uint{file.ID}, false); err != nil {
|
||||
if err := fs.Delete(ctx, []uint{}, []uint{file.ID}, false, false); err != nil {
|
||||
return http.StatusMethodNotAllowed, err
|
||||
}
|
||||
return http.StatusNoContent, nil
|
||||
@@ -311,7 +311,7 @@ func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request, fs *files
|
||||
|
||||
// 尝试作为目录删除
|
||||
if ok, folder := fs.IsPathExist(reqPath); ok {
|
||||
if err := fs.Delete(ctx, []uint{folder.ID}, []uint{}, false); err != nil {
|
||||
if err := fs.Delete(ctx, []uint{folder.ID}, []uint{}, false, false); err != nil {
|
||||
return http.StatusMethodNotAllowed, err
|
||||
}
|
||||
return http.StatusNoContent, nil
|
||||
@@ -783,10 +783,11 @@ const (
|
||||
// infiniteDepth. Parsing any other string returns invalidDepth.
|
||||
//
|
||||
// Different WebDAV methods have further constraints on valid depths:
|
||||
// - PROPFIND has no further restrictions, as per section 9.1.
|
||||
// - COPY accepts only "0" or "infinity", as per section 9.8.3.
|
||||
// - MOVE accepts only "infinity", as per section 9.9.2.
|
||||
// - LOCK accepts only "0" or "infinity", as per section 9.10.3.
|
||||
// - PROPFIND has no further restrictions, as per section 9.1.
|
||||
// - COPY accepts only "0" or "infinity", as per section 9.8.3.
|
||||
// - MOVE accepts only "infinity", as per section 9.9.2.
|
||||
// - LOCK accepts only "0" or "infinity", as per section 9.10.3.
|
||||
//
|
||||
// These constraints are enforced by the handleXxx methods.
|
||||
func parseDepth(s string) int {
|
||||
switch s {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package wopi
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ActonType string
|
||||
|
||||
var (
|
||||
ActionPreview = ActonType("embedview")
|
||||
ActionPreviewFallback = ActonType("view")
|
||||
ActionEdit = ActonType("edit")
|
||||
)
|
||||
|
||||
const (
|
||||
DiscoverResponseCacheKey = "wopi_discover"
|
||||
DiscoverRefreshDuration = 24 * 3600 // 24 hrs
|
||||
)
|
||||
|
||||
func (c *client) AvailableExts() []string {
|
||||
if err := c.checkDiscovery(); err != nil {
|
||||
util.Log().Error("Failed to check WOPI discovery: %s", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
exts := make([]string, 0, len(c.actions))
|
||||
for ext, actions := range c.actions {
|
||||
_, previewable := actions[string(ActionPreview)]
|
||||
_, editable := actions[string(ActionEdit)]
|
||||
_, previewableFallback := actions[string(ActionPreviewFallback)]
|
||||
|
||||
if previewable || editable || previewableFallback {
|
||||
exts = append(exts, strings.TrimPrefix(ext, "."))
|
||||
}
|
||||
}
|
||||
|
||||
return exts
|
||||
}
|
||||
|
||||
// checkDiscovery checks if discovery content is needed to be refreshed.
|
||||
// If so, it will refresh discovery content.
|
||||
func (c *client) checkDiscovery() error {
|
||||
c.mu.RLock()
|
||||
if c.discovery == nil {
|
||||
c.mu.RUnlock()
|
||||
return c.refreshDiscovery()
|
||||
}
|
||||
|
||||
c.mu.RUnlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// refresh Discovery action configs.
|
||||
func (c *client) refreshDiscovery() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
cached, exist := c.cache.Get(DiscoverResponseCacheKey)
|
||||
if exist {
|
||||
cachedDiscovery := cached.(WopiDiscovery)
|
||||
c.discovery = &cachedDiscovery
|
||||
} else {
|
||||
res, err := c.http.Request("GET", c.config.discoveryEndpoint.String(), nil).
|
||||
CheckHTTPResponse(http.StatusOK).GetResponse()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to request discovery endpoint: %w", err)
|
||||
}
|
||||
|
||||
if err := xml.Unmarshal([]byte(res), &c.discovery); err != nil {
|
||||
return fmt.Errorf("failed to parse response discovery endpoint: %w", err)
|
||||
}
|
||||
|
||||
if err := c.cache.Set(DiscoverResponseCacheKey, *c.discovery, DiscoverRefreshDuration); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// construct actions map
|
||||
c.actions = make(map[string]map[string]Action)
|
||||
for _, app := range c.discovery.NetZone.App {
|
||||
for _, action := range app.Action {
|
||||
if action.Ext == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := c.actions["."+action.Ext]; !ok {
|
||||
c.actions["."+action.Ext] = make(map[string]Action)
|
||||
}
|
||||
|
||||
c.actions["."+action.Ext][action.Name] = action
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,70 @@
|
||||
package wopi
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"encoding/xml"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Response content from discovery endpoint.
|
||||
type WopiDiscovery struct {
|
||||
XMLName xml.Name `xml:"wopi-discovery"`
|
||||
Text string `xml:",chardata"`
|
||||
NetZone struct {
|
||||
Text string `xml:",chardata"`
|
||||
Name string `xml:"name,attr"`
|
||||
App []struct {
|
||||
Text string `xml:",chardata"`
|
||||
Name string `xml:"name,attr"`
|
||||
FavIconUrl string `xml:"favIconUrl,attr"`
|
||||
BootstrapperUrl string `xml:"bootstrapperUrl,attr"`
|
||||
AppBootstrapperUrl string `xml:"appBootstrapperUrl,attr"`
|
||||
ApplicationBaseUrl string `xml:"applicationBaseUrl,attr"`
|
||||
StaticResourceOrigin string `xml:"staticResourceOrigin,attr"`
|
||||
CheckLicense string `xml:"checkLicense,attr"`
|
||||
Action []Action `xml:"action"`
|
||||
} `xml:"app"`
|
||||
} `xml:"net-zone"`
|
||||
ProofKey struct {
|
||||
Text string `xml:",chardata"`
|
||||
Oldvalue string `xml:"oldvalue,attr"`
|
||||
Oldmodulus string `xml:"oldmodulus,attr"`
|
||||
Oldexponent string `xml:"oldexponent,attr"`
|
||||
Value string `xml:"value,attr"`
|
||||
Modulus string `xml:"modulus,attr"`
|
||||
Exponent string `xml:"exponent,attr"`
|
||||
} `xml:"proof-key"`
|
||||
}
|
||||
|
||||
type Action struct {
|
||||
Text string `xml:",chardata"`
|
||||
Name string `xml:"name,attr"`
|
||||
Ext string `xml:"ext,attr"`
|
||||
Default string `xml:"default,attr"`
|
||||
Urlsrc string `xml:"urlsrc,attr"`
|
||||
Requires string `xml:"requires,attr"`
|
||||
Targetext string `xml:"targetext,attr"`
|
||||
Progid string `xml:"progid,attr"`
|
||||
UseParent string `xml:"useParent,attr"`
|
||||
Newprogid string `xml:"newprogid,attr"`
|
||||
Newext string `xml:"newext,attr"`
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
AccessToken string
|
||||
AccessTokenTTL int64
|
||||
ActionURL *url.URL
|
||||
}
|
||||
|
||||
type SessionCache struct {
|
||||
AccessToken string
|
||||
FileID uint
|
||||
UserID uint
|
||||
Action ActonType
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register(WopiDiscovery{})
|
||||
gob.Register(Action{})
|
||||
gob.Register(SessionCache{})
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package wopi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/gofrs/uuid"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client interface {
|
||||
// NewSession creates a new document session with access token.
|
||||
NewSession(user *model.User, file *model.File, action ActonType) (*Session, error)
|
||||
// AvailableExts returns a list of file extensions that are supported by WOPI.
|
||||
AvailableExts() []string
|
||||
}
|
||||
|
||||
var (
|
||||
ErrActionNotSupported = errors.New("action not supported by current wopi endpoint")
|
||||
|
||||
Default Client
|
||||
DefaultMu sync.Mutex
|
||||
|
||||
queryPlaceholders = map[string]string{
|
||||
"BUSINESS_USER": "",
|
||||
"DC_LLCC": "lng",
|
||||
"DISABLE_ASYNC": "",
|
||||
"DISABLE_CHAT": "",
|
||||
"EMBEDDED": "true",
|
||||
"FULLSCREEN": "true",
|
||||
"HOST_SESSION_ID": "",
|
||||
"SESSION_CONTEXT": "",
|
||||
"RECORDING": "",
|
||||
"THEME_ID": "darkmode",
|
||||
"UI_LLCC": "lng",
|
||||
"VALIDATOR_TEST_CATEGORY": "",
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
SessionCachePrefix = "wopi_session_"
|
||||
AccessTokenQuery = "access_token"
|
||||
OverwriteHeader = wopiHeaderPrefix + "Override"
|
||||
ServerErrorHeader = wopiHeaderPrefix + "ServerError"
|
||||
RenameRequestHeader = wopiHeaderPrefix + "RequestedName"
|
||||
|
||||
MethodLock = "LOCK"
|
||||
MethodUnlock = "UNLOCK"
|
||||
MethodRefreshLock = "REFRESH_LOCK"
|
||||
MethodRename = "RENAME_FILE"
|
||||
|
||||
wopiSrcPlaceholder = "WOPI_SOURCE"
|
||||
wopiSrcParamDefault = "WOPISrc"
|
||||
sessionExpiresPadding = 10
|
||||
wopiHeaderPrefix = "X-WOPI-"
|
||||
)
|
||||
|
||||
// Init initializes a new global WOPI client.
|
||||
func Init() {
|
||||
settings := model.GetSettingByNames("wopi_endpoint", "wopi_enabled")
|
||||
if !model.IsTrueVal(settings["wopi_enabled"]) {
|
||||
DefaultMu.Lock()
|
||||
Default = nil
|
||||
DefaultMu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
cache.Deletes([]string{DiscoverResponseCacheKey}, "")
|
||||
wopiClient, err := NewClient(settings["wopi_endpoint"], cache.Store, request.NewClient())
|
||||
if err != nil {
|
||||
util.Log().Error("Failed to initialize WOPI client: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
DefaultMu.Lock()
|
||||
Default = wopiClient
|
||||
DefaultMu.Unlock()
|
||||
}
|
||||
|
||||
type client struct {
|
||||
cache cache.Driver
|
||||
http request.Client
|
||||
mu sync.RWMutex
|
||||
|
||||
discovery *WopiDiscovery
|
||||
actions map[string]map[string]Action
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
type config struct {
|
||||
discoveryEndpoint *url.URL
|
||||
}
|
||||
|
||||
func NewClient(endpoint string, cache cache.Driver, http request.Client) (Client, error) {
|
||||
endpointUrl, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse WOPI endpoint: %s", err)
|
||||
}
|
||||
|
||||
return &client{
|
||||
cache: cache,
|
||||
http: http,
|
||||
config: config{
|
||||
discoveryEndpoint: endpointUrl,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *client) NewSession(user *model.User, file *model.File, action ActonType) (*Session, error) {
|
||||
if err := c.checkDiscovery(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
ext := path.Ext(file.Name)
|
||||
availableActions, ok := c.actions[ext]
|
||||
if !ok {
|
||||
return nil, ErrActionNotSupported
|
||||
}
|
||||
|
||||
var (
|
||||
actionConfig Action
|
||||
)
|
||||
fallbackOrder := []ActonType{action, ActionPreview, ActionPreviewFallback, ActionEdit}
|
||||
for _, a := range fallbackOrder {
|
||||
if actionConfig, ok = availableActions[string(a)]; ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if actionConfig.Urlsrc == "" {
|
||||
return nil, ErrActionNotSupported
|
||||
}
|
||||
|
||||
// Generate WOPI REST endpoint for given file
|
||||
baseURL := model.GetSiteURL()
|
||||
linkPath, err := url.Parse(fmt.Sprintf("/api/v3/wopi/files/%s", hashid.HashID(file.ID, hashid.FileID)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
actionUrl, err := generateActionUrl(actionConfig.Urlsrc, baseURL.ResolveReference(linkPath).String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create document session
|
||||
sessionID := uuid.Must(uuid.NewV4())
|
||||
token := util.RandStringRunes(64)
|
||||
ttl := model.GetIntSetting("wopi_session_timeout", 36000)
|
||||
session := &SessionCache{
|
||||
AccessToken: fmt.Sprintf("%s.%s", sessionID, token),
|
||||
FileID: file.ID,
|
||||
UserID: user.ID,
|
||||
Action: action,
|
||||
}
|
||||
err = c.cache.Set(SessionCachePrefix+sessionID.String(), *session, ttl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create document session: %w", err)
|
||||
}
|
||||
|
||||
sessionRes := &Session{
|
||||
AccessToken: session.AccessToken,
|
||||
ActionURL: actionUrl,
|
||||
AccessTokenTTL: time.Now().Add(time.Duration(ttl-sessionExpiresPadding) * time.Second).UnixMilli(),
|
||||
}
|
||||
|
||||
return sessionRes, nil
|
||||
}
|
||||
|
||||
// Replace query parameters in action URL template. Some placeholders need to be replaced
|
||||
// at the frontend, e.g. `THEME_ID`.
|
||||
func generateActionUrl(src string, fileSrc string) (*url.URL, error) {
|
||||
src = strings.ReplaceAll(src, "<", "")
|
||||
src = strings.ReplaceAll(src, ">", "")
|
||||
actionUrl, err := url.Parse(src)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse action url: %s", err)
|
||||
}
|
||||
|
||||
queries := actionUrl.Query()
|
||||
srcReplaced := false
|
||||
queryReplaced := url.Values{}
|
||||
for k := range queries {
|
||||
if placeholder, ok := queryPlaceholders[queries.Get(k)]; ok {
|
||||
if placeholder != "" {
|
||||
queryReplaced.Set(k, placeholder)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if queries.Get(k) == wopiSrcPlaceholder {
|
||||
queryReplaced.Set(k, fileSrc)
|
||||
srcReplaced = true
|
||||
continue
|
||||
}
|
||||
|
||||
queryReplaced.Set(k, queries.Get(k))
|
||||
}
|
||||
|
||||
if !srcReplaced {
|
||||
queryReplaced.Set(wopiSrcParamDefault, fileSrc)
|
||||
}
|
||||
|
||||
actionUrl.RawQuery = queryReplaced.Encode()
|
||||
return actionUrl, nil
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package wopi
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/mocks/cachemock"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/mocks/requestmock"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/request"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/stretchr/testify/assert"
|
||||
testMock "github.com/stretchr/testify/mock"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var mock sqlmock.Sqlmock
|
||||
|
||||
// TestMain 初始化数据库Mock
|
||||
func TestMain(m *testing.M) {
|
||||
var db *sql.DB
|
||||
var err error
|
||||
db, mock, err = sqlmock.New()
|
||||
if err != nil {
|
||||
panic("An error was not expected when opening a stub database connection")
|
||||
}
|
||||
model.DB, _ = gorm.Open("mysql", db)
|
||||
defer db.Close()
|
||||
m.Run()
|
||||
}
|
||||
|
||||
func TestNewSession(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
endpoint, _ := url.Parse("http://localhost:8001/hosting/discovery")
|
||||
client := &client{
|
||||
cache: cache.NewMemoStore(),
|
||||
config: config{
|
||||
discoveryEndpoint: endpoint,
|
||||
},
|
||||
}
|
||||
|
||||
// Discovery failed
|
||||
{
|
||||
expectedErr := errors.New("error")
|
||||
mockHttp := &requestmock.RequestMock{}
|
||||
client.http = mockHttp
|
||||
mockHttp.On(
|
||||
"Request",
|
||||
"GET",
|
||||
endpoint.String(),
|
||||
testMock.Anything,
|
||||
testMock.Anything,
|
||||
).Return(&request.Response{
|
||||
Err: expectedErr,
|
||||
})
|
||||
res, err := client.NewSession(&model.User{}, &model.File{}, ActionPreview)
|
||||
a.Nil(res)
|
||||
a.ErrorIs(err, expectedErr)
|
||||
mockHttp.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// not supported ext
|
||||
{
|
||||
client.discovery = &WopiDiscovery{}
|
||||
client.actions = make(map[string]map[string]Action)
|
||||
res, err := client.NewSession(&model.User{}, &model.File{}, ActionPreview)
|
||||
a.Nil(res)
|
||||
a.ErrorIs(err, ErrActionNotSupported)
|
||||
}
|
||||
|
||||
// preferred action not supported
|
||||
{
|
||||
client.discovery = &WopiDiscovery{}
|
||||
client.actions = map[string]map[string]Action{
|
||||
".doc": {},
|
||||
}
|
||||
res, err := client.NewSession(&model.User{}, &model.File{Name: "1.doc"}, ActionPreview)
|
||||
a.Nil(res)
|
||||
a.ErrorIs(err, ErrActionNotSupported)
|
||||
}
|
||||
|
||||
// src url cannot be parsed
|
||||
{
|
||||
client.discovery = &WopiDiscovery{}
|
||||
client.actions = map[string]map[string]Action{
|
||||
".doc": {
|
||||
string(ActionPreviewFallback): Action{
|
||||
Urlsrc: string([]byte{0x7f}),
|
||||
},
|
||||
},
|
||||
}
|
||||
res, err := client.NewSession(&model.User{}, &model.File{Name: "1.doc"}, ActionEdit)
|
||||
a.Nil(res)
|
||||
a.ErrorContains(err, "invalid control character in URL")
|
||||
}
|
||||
|
||||
// all pass - default placeholder
|
||||
{
|
||||
client.discovery = &WopiDiscovery{}
|
||||
client.actions = map[string]map[string]Action{
|
||||
".doc": {
|
||||
string(ActionPreviewFallback): Action{
|
||||
Urlsrc: "https://doc.com/doc",
|
||||
},
|
||||
},
|
||||
}
|
||||
res, err := client.NewSession(&model.User{}, &model.File{Name: "1.doc"}, ActionEdit)
|
||||
a.NotNil(res)
|
||||
a.NoError(err)
|
||||
resUrl := res.ActionURL.String()
|
||||
a.Contains(resUrl, wopiSrcParamDefault)
|
||||
}
|
||||
|
||||
// all pass - with placeholders
|
||||
{
|
||||
client.discovery = &WopiDiscovery{}
|
||||
client.actions = map[string]map[string]Action{
|
||||
".doc": {
|
||||
string(ActionPreviewFallback): Action{
|
||||
Urlsrc: "https://doc.com/doc?origin=preserved&<dc=DC_LLCC&><notsuported=DISABLE_ASYNC&><src=WOPI_SOURCE&>",
|
||||
},
|
||||
},
|
||||
}
|
||||
res, err := client.NewSession(&model.User{}, &model.File{Name: "1.doc"}, ActionEdit)
|
||||
a.NotNil(res)
|
||||
a.NoError(err)
|
||||
resUrl := res.ActionURL.String()
|
||||
a.Contains(resUrl, "origin=preserved")
|
||||
a.Contains(resUrl, "dc=lng")
|
||||
a.Contains(resUrl, "src=")
|
||||
a.NotContains(resUrl, "notsuported")
|
||||
}
|
||||
|
||||
// cache operation failed
|
||||
{
|
||||
mockCache := &cachemock.CacheClientMock{}
|
||||
expectedErr := errors.New("error")
|
||||
client.cache = mockCache
|
||||
client.discovery = &WopiDiscovery{}
|
||||
client.actions = map[string]map[string]Action{
|
||||
".doc": {
|
||||
string(ActionPreviewFallback): Action{
|
||||
Urlsrc: "https://doc.com/doc",
|
||||
},
|
||||
},
|
||||
}
|
||||
mockCache.On("Set", testMock.Anything, testMock.Anything, testMock.Anything).Return(expectedErr)
|
||||
res, err := client.NewSession(&model.User{}, &model.File{Name: "1.doc"}, ActionEdit)
|
||||
a.Nil(res)
|
||||
a.ErrorIs(err, expectedErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
|
||||
// not enabled
|
||||
{
|
||||
a.Nil(Default)
|
||||
Default = &client{}
|
||||
Init()
|
||||
a.Nil(Default)
|
||||
}
|
||||
|
||||
// throw error
|
||||
{
|
||||
a.Nil(Default)
|
||||
cache.Set("setting_wopi_enabled", "1", 0)
|
||||
cache.Set("setting_wopi_endpoint", string([]byte{0x7f}), 0)
|
||||
Init()
|
||||
a.Nil(Default)
|
||||
}
|
||||
|
||||
// all pass
|
||||
{
|
||||
a.Nil(Default)
|
||||
cache.Set("setting_wopi_enabled", "1", 0)
|
||||
cache.Set("setting_wopi_endpoint", "", 0)
|
||||
Init()
|
||||
a.NotNil(Default)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/email"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/wopi"
|
||||
"github.com/cloudreve/Cloudreve/v3/service/admin"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -79,6 +80,8 @@ func AdminReloadService(c *gin.Context) {
|
||||
email.Init()
|
||||
case "aria2":
|
||||
aria2.Init(true, cluster.Default, mq.GlobalMQ)
|
||||
case "wopi":
|
||||
wopi.Init()
|
||||
}
|
||||
|
||||
c.JSON(200, serializer.Response{})
|
||||
|
||||
@@ -236,7 +236,7 @@ func GetDocPreview(c *gin.Context) {
|
||||
|
||||
var service explorer.FileIDService
|
||||
if err := c.ShouldBindUri(&service); err == nil {
|
||||
res := service.CreateDocPreviewSession(ctx, c)
|
||||
res := service.CreateDocPreviewSession(ctx, c, true)
|
||||
c.JSON(200, res)
|
||||
} else {
|
||||
c.JSON(200, ErrorResponse(err))
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/wopi"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mojocn/base64Captcha"
|
||||
)
|
||||
@@ -27,16 +28,22 @@ func SiteConfig(c *gin.Context) {
|
||||
"captcha_type",
|
||||
"captcha_TCaptcha_CaptchaAppId",
|
||||
"register_enabled",
|
||||
"show_app_promotion",
|
||||
)
|
||||
|
||||
var wopiExts []string
|
||||
if wopi.Default != nil {
|
||||
wopiExts = wopi.Default.AvailableExts()
|
||||
}
|
||||
|
||||
// 如果已登录,则同时返回用户信息和标签
|
||||
user, _ := c.Get("user")
|
||||
if user, ok := user.(*model.User); ok {
|
||||
c.JSON(200, serializer.BuildSiteConfig(siteConfig, user))
|
||||
c.JSON(200, serializer.BuildSiteConfig(siteConfig, user, wopiExts))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, serializer.BuildSiteConfig(siteConfig, nil))
|
||||
c.JSON(200, serializer.BuildSiteConfig(siteConfig, nil, wopiExts))
|
||||
}
|
||||
|
||||
// Ping 状态检查页面
|
||||
|
||||
@@ -377,3 +377,22 @@ func UserInit2FA(c *gin.Context) {
|
||||
c.JSON(200, ErrorResponse(err))
|
||||
}
|
||||
}
|
||||
|
||||
// UserPrepareCopySession generates URL for copy session
|
||||
func UserPrepareCopySession(c *gin.Context) {
|
||||
var service user.CopySessionService
|
||||
res := service.Prepare(c, CurrentUser(c))
|
||||
c.JSON(200, res)
|
||||
|
||||
}
|
||||
|
||||
// UserPerformCopySession copy to create new session or refresh current session
|
||||
func UserPerformCopySession(c *gin.Context) {
|
||||
var service user.CopySessionService
|
||||
if err := c.ShouldBindUri(&service); err == nil {
|
||||
res := service.Copy(c)
|
||||
c.JSON(200, res)
|
||||
} else {
|
||||
c.JSON(200, ErrorResponse(err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/webdav"
|
||||
"github.com/cloudreve/Cloudreve/v3/service/setting"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -39,6 +40,15 @@ func ServeWebDAV(c *gin.Context) {
|
||||
fs.Root = root
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否只读
|
||||
if application.Readonly {
|
||||
switch c.Request.Method {
|
||||
case "DELETE", "PUT", "MKCOL", "COPY", "MOVE":
|
||||
c.Status(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handler.ServeHTTP(c.Writer, c.Request, fs)
|
||||
@@ -66,6 +76,17 @@ func DeleteWebDAVAccounts(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateWebDAVAccountsReadonly 更改WebDAV账户只读性
|
||||
func UpdateWebDAVAccountsReadonly(c *gin.Context) {
|
||||
var service setting.WebDAVAccountUpdateReadonlyService
|
||||
if err := c.ShouldBindJSON(&service); err == nil {
|
||||
res := service.Update(c, CurrentUser(c))
|
||||
c.JSON(200, res)
|
||||
} else {
|
||||
c.JSON(200, ErrorResponse(err))
|
||||
}
|
||||
}
|
||||
|
||||
// CreateWebDAVAccounts 创建WebDAV账户
|
||||
func CreateWebDAVAccounts(c *gin.Context) {
|
||||
var service setting.WebDAVAccountCreateService
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/wopi"
|
||||
"github.com/cloudreve/Cloudreve/v3/service/explorer"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// CheckFileInfo Get file info
|
||||
func CheckFileInfo(c *gin.Context) {
|
||||
var service explorer.WopiService
|
||||
res, err := service.FileInfo(c)
|
||||
if err != nil {
|
||||
c.Status(http.StatusInternalServerError)
|
||||
c.Header(wopi.ServerErrorHeader, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, res)
|
||||
}
|
||||
|
||||
// GetFile Get file content
|
||||
func GetFile(c *gin.Context) {
|
||||
var service explorer.WopiService
|
||||
err := service.GetFile(c)
|
||||
if err != nil {
|
||||
c.Status(http.StatusInternalServerError)
|
||||
c.Header(wopi.ServerErrorHeader, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// PutFile Puts file content
|
||||
func PutFile(c *gin.Context) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
service := &explorer.FileIDService{}
|
||||
res := service.PutContent(ctx, c)
|
||||
switch res.Code {
|
||||
case serializer.CodeFileTooLarge:
|
||||
c.Status(http.StatusRequestEntityTooLarge)
|
||||
c.Header(wopi.ServerErrorHeader, res.Error)
|
||||
case serializer.CodeNotFound:
|
||||
c.Status(http.StatusNotFound)
|
||||
c.Header(wopi.ServerErrorHeader, res.Error)
|
||||
case 0:
|
||||
c.Status(http.StatusOK)
|
||||
default:
|
||||
c.Status(http.StatusInternalServerError)
|
||||
c.Header(wopi.ServerErrorHeader, res.Error)
|
||||
}
|
||||
}
|
||||
|
||||
// ModifyFile Modify file properties
|
||||
func ModifyFile(c *gin.Context) {
|
||||
action := c.GetHeader(wopi.OverwriteHeader)
|
||||
switch action {
|
||||
case wopi.MethodLock, wopi.MethodRefreshLock, wopi.MethodUnlock:
|
||||
c.Status(http.StatusOK)
|
||||
return
|
||||
case wopi.MethodRename:
|
||||
var service explorer.WopiService
|
||||
err := service.Rename(c)
|
||||
if err != nil {
|
||||
c.Status(http.StatusInternalServerError)
|
||||
c.Header(wopi.ServerErrorHeader, err.Error())
|
||||
return
|
||||
}
|
||||
default:
|
||||
c.Status(http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
}
|
||||
+40
-2
@@ -3,10 +3,12 @@ package routers
|
||||
import (
|
||||
"github.com/cloudreve/Cloudreve/v3/middleware"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
wopi2 "github.com/cloudreve/Cloudreve/v3/pkg/wopi"
|
||||
"github.com/cloudreve/Cloudreve/v3/routers/controllers"
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-contrib/gzip"
|
||||
@@ -206,6 +208,7 @@ func InitMasterRouter() *gin.Engine {
|
||||
// 获取用户头像
|
||||
user.GET("avatar/:id/:size",
|
||||
middleware.HashID(hashid.UserID),
|
||||
middleware.StaticResourceCache(),
|
||||
controllers.GetUserAvatar,
|
||||
)
|
||||
}
|
||||
@@ -217,14 +220,28 @@ func InitMasterRouter() *gin.Engine {
|
||||
file := sign.Group("file")
|
||||
{
|
||||
// 文件外链(直接输出文件数据)
|
||||
file.GET("get/:id/:name", middleware.Sandbox(), controllers.AnonymousGetContent)
|
||||
file.GET("get/:id/:name",
|
||||
middleware.Sandbox(),
|
||||
middleware.StaticResourceCache(),
|
||||
controllers.AnonymousGetContent,
|
||||
)
|
||||
// 文件外链(301跳转)
|
||||
file.GET("source/:id/:name", controllers.AnonymousPermLinkDeprecated)
|
||||
// 下载文件
|
||||
file.GET("download/:id", controllers.Download)
|
||||
file.GET("download/:id",
|
||||
middleware.StaticResourceCache(),
|
||||
controllers.Download,
|
||||
)
|
||||
// 打包并下载文件
|
||||
file.GET("archive/:sessionID/archive.zip", controllers.DownloadArchive)
|
||||
}
|
||||
|
||||
// Copy user session
|
||||
sign.GET(
|
||||
"user/session/copy/:id",
|
||||
middleware.MobileRequestOnly(),
|
||||
controllers.UserPerformCopySession,
|
||||
)
|
||||
}
|
||||
|
||||
// 从机的 RPC 通信
|
||||
@@ -370,6 +387,22 @@ func InitMasterRouter() *gin.Engine {
|
||||
v3.Group("share").GET("search", controllers.SearchShare)
|
||||
}
|
||||
|
||||
wopi := v3.Group(
|
||||
"wopi",
|
||||
middleware.HashID(hashid.FileID),
|
||||
middleware.WopiAccessValidation(wopi2.Default, cache.Store),
|
||||
)
|
||||
{
|
||||
// 获取文件信息
|
||||
wopi.GET("files/:id", controllers.CheckFileInfo)
|
||||
// 获取文件内容
|
||||
wopi.GET("files/:id/contents", controllers.GetFile)
|
||||
// 更新文件内容
|
||||
wopi.POST("files/:id/contents", middleware.WopiWriteAccess(), controllers.PutFile)
|
||||
// 通用文件操作
|
||||
wopi.POST("files/:id", middleware.WopiWriteAccess(), controllers.ModifyFile)
|
||||
}
|
||||
|
||||
// 需要登录保护的
|
||||
auth := v3.Group("")
|
||||
auth.Use(middleware.AuthRequired())
|
||||
@@ -515,6 +548,9 @@ func InitMasterRouter() *gin.Engine {
|
||||
user.GET("storage", controllers.UserStorage)
|
||||
// 退出登录
|
||||
user.DELETE("session", controllers.UserSignOut)
|
||||
// Generate temp URL for copying client-side session, used in adding accounts
|
||||
// for mobile App.
|
||||
user.GET("session", controllers.UserPrepareCopySession)
|
||||
|
||||
// WebAuthn 注册相关
|
||||
authn := user.Group("authn",
|
||||
@@ -663,6 +699,8 @@ func InitMasterRouter() *gin.Engine {
|
||||
webdav.POST("accounts", controllers.CreateWebDAVAccounts)
|
||||
// 删除账号
|
||||
webdav.DELETE("accounts/:id", controllers.DeleteWebDAVAccounts)
|
||||
// 更新账号可读性
|
||||
webdav.PATCH("accounts", controllers.UpdateWebDAVAccountsReadonly)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+18
-10
@@ -19,8 +19,9 @@ type FileService struct {
|
||||
|
||||
// FileBatchService 文件批量操作服务
|
||||
type FileBatchService struct {
|
||||
ID []uint `json:"id" binding:"min=1"`
|
||||
Force bool `json:"force"`
|
||||
ID []uint `json:"id" binding:"min=1"`
|
||||
Force bool `json:"force"`
|
||||
UnlinkOnly bool `json:"unlink"`
|
||||
}
|
||||
|
||||
// ListFolderService 列目录结构
|
||||
@@ -103,15 +104,22 @@ func (service *FileBatchService) Delete(c *gin.Context) serializer.Response {
|
||||
// 异步执行删除
|
||||
go func(files map[uint][]model.File) {
|
||||
for uid, file := range files {
|
||||
var (
|
||||
fs *filesystem.FileSystem
|
||||
err error
|
||||
)
|
||||
user, err := model.GetUserByID(uid)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
fs, err := filesystem.NewFileSystem(&user)
|
||||
if err != nil {
|
||||
fs.Recycle()
|
||||
continue
|
||||
fs, err = filesystem.NewAnonymousFileSystem()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
fs, err = filesystem.NewFileSystem(&user)
|
||||
if err != nil {
|
||||
fs.Recycle()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 汇总文件ID
|
||||
@@ -121,7 +129,7 @@ func (service *FileBatchService) Delete(c *gin.Context) serializer.Response {
|
||||
}
|
||||
|
||||
// 执行删除
|
||||
fs.Delete(context.Background(), []uint{}, ids, service.Force)
|
||||
fs.Delete(context.Background(), []uint{}, ids, service.Force, service.UnlinkOnly)
|
||||
fs.Recycle()
|
||||
}
|
||||
}(userFile)
|
||||
|
||||
+12
-4
@@ -318,12 +318,20 @@ func (service *AdminListService) Policies() serializer.Response {
|
||||
|
||||
// 统计每个策略的文件使用
|
||||
statics := make(map[uint][2]int, len(res))
|
||||
policyIds := make([]uint, 0, len(res))
|
||||
for i := 0; i < len(res); i++ {
|
||||
policyIds = append(policyIds, res[i].ID)
|
||||
}
|
||||
|
||||
rows, _ := model.DB.Model(&model.File{}).Where("policy_id in (?)", policyIds).
|
||||
Select("policy_id,count(id),sum(size)").Group("policy_id").Rows()
|
||||
|
||||
for rows.Next() {
|
||||
policyId := uint(0)
|
||||
total := [2]int{}
|
||||
row := model.DB.Model(&model.File{}).Where("policy_id = ?", res[i].ID).
|
||||
Select("count(id),sum(size)").Row()
|
||||
row.Scan(&total[0], &total[1])
|
||||
statics[res[i].ID] = total
|
||||
rows.Scan(&policyId, &total[0], &total[1])
|
||||
|
||||
statics[policyId] = total
|
||||
}
|
||||
|
||||
return serializer.Response{Data: map[string]interface{}{
|
||||
|
||||
@@ -66,7 +66,7 @@ func (service *UserBatchService) Delete() serializer.Response {
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "User's root folder not exist", err)
|
||||
}
|
||||
fs.Delete(context.Background(), []uint{root.ID}, []uint{}, false)
|
||||
fs.Delete(context.Background(), []uint{root.ID}, []uint{}, false, false)
|
||||
|
||||
// 删除相关任务
|
||||
model.DB.Where("user_id = ?", uid).Delete(&model.Download{})
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/wopi"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -192,7 +193,7 @@ func (service *FileAnonymousGetService) Source(ctx context.Context, c *gin.Conte
|
||||
}
|
||||
|
||||
// CreateDocPreviewSession 创建DOC文件预览会话,返回预览地址
|
||||
func (service *FileIDService) CreateDocPreviewSession(ctx context.Context, c *gin.Context) serializer.Response {
|
||||
func (service *FileIDService) CreateDocPreviewSession(ctx context.Context, c *gin.Context, editable bool) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystemFromContext(c)
|
||||
if err != nil {
|
||||
@@ -226,16 +227,47 @@ func (service *FileIDService) CreateDocPreviewSession(ctx context.Context, c *gi
|
||||
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
|
||||
}
|
||||
|
||||
var resp serializer.DocPreviewSession
|
||||
|
||||
// Use WOPI preview if available
|
||||
if model.IsTrueVal(model.GetSettingByName("wopi_enabled")) && wopi.Default != nil {
|
||||
maxSize := model.GetIntSetting("maxEditSize", 0)
|
||||
if maxSize > 0 && fs.FileTarget[0].Size > uint64(maxSize) {
|
||||
return serializer.Err(serializer.CodeFileTooLarge, "", nil)
|
||||
}
|
||||
|
||||
action := wopi.ActionPreview
|
||||
if editable {
|
||||
action = wopi.ActionEdit
|
||||
}
|
||||
|
||||
session, err := wopi.Default.NewSession(fs.User, &fs.FileTarget[0], action)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to create WOPI session", err)
|
||||
}
|
||||
|
||||
resp.URL = session.ActionURL.String()
|
||||
resp.AccessTokenTTL = session.AccessTokenTTL
|
||||
resp.AccessToken = session.AccessToken
|
||||
return serializer.Response{
|
||||
Code: 0,
|
||||
Data: resp,
|
||||
}
|
||||
}
|
||||
|
||||
// 生成最终的预览器地址
|
||||
srcB64 := base64.StdEncoding.EncodeToString([]byte(downloadURL))
|
||||
srcEncoded := url.QueryEscape(downloadURL)
|
||||
srcB64Encoded := url.QueryEscape(srcB64)
|
||||
resp.URL = util.Replace(map[string]string{
|
||||
"{$src}": srcEncoded,
|
||||
"{$srcB64}": srcB64Encoded,
|
||||
"{$name}": url.QueryEscape(fs.FileTarget[0].Name),
|
||||
}, model.GetSettingByName("office_preview_service"))
|
||||
|
||||
return serializer.Response{
|
||||
Code: 0,
|
||||
Data: util.Replace(map[string]string{
|
||||
"{$src}": srcEncoded,
|
||||
"{$srcB64}": srcB64Encoded,
|
||||
}, model.GetSettingByName("office_preview_service")),
|
||||
Data: resp,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,18 +437,18 @@ func (service *FileIDService) PutContent(ctx context.Context, c *gin.Context) se
|
||||
fileData.Mode &= ^fsctx.Overwrite
|
||||
fs.Use("AfterUpload", filesystem.HookUpdateSourceName)
|
||||
fs.Use("AfterUploadCanceled", filesystem.HookUpdateSourceName)
|
||||
fs.Use("AfterUploadCanceled", filesystem.HookCleanFileContent)
|
||||
fs.Use("AfterUploadCanceled", filesystem.HookClearFileSize)
|
||||
fs.Use("AfterValidateFailed", filesystem.HookUpdateSourceName)
|
||||
fs.Use("AfterValidateFailed", filesystem.HookCleanFileContent)
|
||||
fs.Use("AfterValidateFailed", filesystem.HookClearFileSize)
|
||||
}
|
||||
|
||||
// 给文件系统分配钩子
|
||||
fs.Use("BeforeUpload", filesystem.HookResetPolicy)
|
||||
fs.Use("BeforeUpload", filesystem.HookValidateFile)
|
||||
fs.Use("BeforeUpload", filesystem.HookValidateCapacityDiff)
|
||||
fs.Use("AfterUploadCanceled", filesystem.HookCleanFileContent)
|
||||
fs.Use("AfterUploadCanceled", filesystem.HookClearFileSize)
|
||||
fs.Use("AfterUpload", filesystem.GenericAfterUpdate)
|
||||
fs.Use("AfterValidateFailed", filesystem.HookCleanFileContent)
|
||||
fs.Use("AfterValidateFailed", filesystem.HookClearFileSize)
|
||||
|
||||
// 执行上传
|
||||
uploadCtx = context.WithValue(uploadCtx, fsctx.FileModelCtx, originFile[0])
|
||||
|
||||
@@ -41,9 +41,11 @@ type ItemService struct {
|
||||
|
||||
// ItemIDService 处理多文件/目录相关服务,字段值为HashID,可通过Raw()方法获取原始ID
|
||||
type ItemIDService struct {
|
||||
Items []string `json:"items"`
|
||||
Dirs []string `json:"dirs"`
|
||||
Source *ItemService
|
||||
Items []string `json:"items"`
|
||||
Dirs []string `json:"dirs"`
|
||||
Source *ItemService
|
||||
Force bool `json:"force"`
|
||||
UnlinkOnly bool `json:"unlink"`
|
||||
}
|
||||
|
||||
// ItemCompressService 文件压缩任务服务
|
||||
@@ -272,9 +274,15 @@ func (service *ItemIDService) Delete(ctx context.Context, c *gin.Context) serial
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
force, unlink := false, false
|
||||
if fs.User.Group.OptionsSerialized.AdvanceDelete {
|
||||
force = service.Force
|
||||
unlink = service.UnlinkOnly
|
||||
}
|
||||
|
||||
// 删除对象
|
||||
items := service.Raw()
|
||||
err = fs.Delete(ctx, items.Dirs, items.Items, false)
|
||||
err = fs.Delete(ctx, items.Dirs, items.Items, force, unlink)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ func (service *UploadSessionService) Delete(ctx context.Context, c *gin.Context)
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
if err := fs.Delete(ctx, []uint{}, []uint{file.ID}, false); err != nil {
|
||||
if err := fs.Delete(ctx, []uint{}, []uint{file.ID}, false, false); err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to delete upload session", err)
|
||||
}
|
||||
|
||||
@@ -283,7 +283,7 @@ func DeleteAllUploadSession(ctx context.Context, c *gin.Context) serializer.Resp
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
if err := fs.Delete(ctx, []uint{}, fileIDs, false); err != nil {
|
||||
if err := fs.Delete(ctx, []uint{}, fileIDs, false, false); err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to cleanup upload session", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package explorer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cloudreve/Cloudreve/v3/middleware"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/wopi"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type WopiService struct {
|
||||
}
|
||||
|
||||
func (service *WopiService) Rename(c *gin.Context) error {
|
||||
fs, _, err := service.prepareFs(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer fs.Recycle()
|
||||
|
||||
return fs.Rename(c, []uint{}, []uint{c.MustGet("object_id").(uint)}, c.GetHeader(wopi.RenameRequestHeader))
|
||||
}
|
||||
|
||||
func (service *WopiService) GetFile(c *gin.Context) error {
|
||||
fs, _, err := service.prepareFs(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer fs.Recycle()
|
||||
|
||||
resp, err := fs.Preview(c, fs.FileTarget[0].ID, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to pull file content: %w", err)
|
||||
}
|
||||
|
||||
// 重定向到文件源
|
||||
if resp.Redirect {
|
||||
return fmt.Errorf("redirect not supported in WOPI")
|
||||
}
|
||||
|
||||
// 直接返回文件内容
|
||||
defer resp.Content.Close()
|
||||
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
http.ServeContent(c.Writer, c.Request, fs.FileTarget[0].Name, fs.FileTarget[0].UpdatedAt, resp.Content)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *WopiService) FileInfo(c *gin.Context) (*serializer.WopiFileInfo, error) {
|
||||
fs, session, err := service.prepareFs(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer fs.Recycle()
|
||||
|
||||
parent, err := model.GetFoldersByIDs([]uint{fs.FileTarget[0].FolderID}, fs.User.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(parent) == 0 {
|
||||
return nil, fmt.Errorf("failed to find parent folder")
|
||||
}
|
||||
|
||||
parent[0].TraceRoot()
|
||||
siteUrl := model.GetSiteURL()
|
||||
|
||||
// Generate url for parent folder
|
||||
parentUrl := model.GetSiteURL()
|
||||
parentUrl.Path = "/home"
|
||||
query := parentUrl.Query()
|
||||
query.Set("path", parent[0].Position)
|
||||
parentUrl.RawQuery = query.Encode()
|
||||
|
||||
info := &serializer.WopiFileInfo{
|
||||
BaseFileName: fs.FileTarget[0].Name,
|
||||
Version: fs.FileTarget[0].Model.UpdatedAt.String(),
|
||||
BreadcrumbBrandName: model.GetSettingByName("siteName"),
|
||||
BreadcrumbBrandUrl: siteUrl.String(),
|
||||
FileSharingPostMessage: false,
|
||||
PostMessageOrigin: "*",
|
||||
FileNameMaxLength: 256,
|
||||
LastModifiedTime: fs.FileTarget[0].Model.UpdatedAt.Format(time.RFC3339),
|
||||
IsAnonymousUser: true,
|
||||
ReadOnly: true,
|
||||
ClosePostMessage: true,
|
||||
Size: int64(fs.FileTarget[0].Size),
|
||||
OwnerId: hashid.HashID(fs.FileTarget[0].UserID, hashid.UserID),
|
||||
}
|
||||
|
||||
if session.Action == wopi.ActionEdit {
|
||||
info.FileSharingPostMessage = true
|
||||
info.IsAnonymousUser = false
|
||||
info.SupportsRename = true
|
||||
info.SupportsReviewing = true
|
||||
info.SupportsUpdate = true
|
||||
info.UserFriendlyName = fs.User.Nick
|
||||
info.UserId = hashid.HashID(fs.User.ID, hashid.UserID)
|
||||
info.UserCanRename = true
|
||||
info.UserCanReview = true
|
||||
info.UserCanWrite = true
|
||||
info.ReadOnly = false
|
||||
info.BreadcrumbFolderName = parent[0].Name
|
||||
info.BreadcrumbFolderUrl = parentUrl.String()
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (service *WopiService) prepareFs(c *gin.Context) (*filesystem.FileSystem, *wopi.SessionCache, error) {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystemFromContext(c)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
session := c.MustGet(middleware.WopiSessionCtx).(*wopi.SessionCache)
|
||||
if err := fs.SetTargetFileByIDs([]uint{session.FileID}); err != nil {
|
||||
fs.Recycle()
|
||||
return nil, nil, fmt.Errorf("failed to find file: %w", err)
|
||||
}
|
||||
|
||||
maxSize := model.GetIntSetting("maxEditSize", 0)
|
||||
if maxSize > 0 && fs.FileTarget[0].Size > uint64(maxSize) {
|
||||
return nil, nil, errors.New("file too large")
|
||||
}
|
||||
|
||||
return fs, session, nil
|
||||
}
|
||||
@@ -22,6 +22,12 @@ type WebDAVAccountCreateService struct {
|
||||
Name string `json:"name" binding:"required,min=1,max=255"`
|
||||
}
|
||||
|
||||
// WebDAVAccountUpdateReadonlyService WebDAV 修改只读性服务
|
||||
type WebDAVAccountUpdateReadonlyService struct {
|
||||
ID uint `json:"id" binding:"required,min=1"`
|
||||
Readonly bool `json:"readonly"`
|
||||
}
|
||||
|
||||
// WebDAVMountCreateService WebDAV 挂载创建服务
|
||||
type WebDAVMountCreateService struct {
|
||||
Path string `json:"path" binding:"required,min=1,max=65535"`
|
||||
@@ -56,6 +62,14 @@ func (service *WebDAVAccountService) Delete(c *gin.Context, user *model.User) se
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Update 修改WebDAV账户的只读性
|
||||
func (service *WebDAVAccountUpdateReadonlyService) Update(c *gin.Context, user *model.User) serializer.Response {
|
||||
model.UpdateWebDAVAccountReadonlyByID(service.ID, user.ID, service.Readonly)
|
||||
return serializer.Response{Data: map[string]bool{
|
||||
"readonly": service.Readonly,
|
||||
}}
|
||||
}
|
||||
|
||||
// Accounts 列出WebDAV账号
|
||||
func (service *WebDAVListService) Accounts(c *gin.Context, user *model.User) serializer.Response {
|
||||
accounts := model.ListWebDAVAccounts(user.ID)
|
||||
|
||||
@@ -221,7 +221,7 @@ func (service *Service) CreateDocPreviewSession(c *gin.Context) serializer.Respo
|
||||
}
|
||||
subService := explorer.FileIDService{}
|
||||
|
||||
return subService.CreateDocPreviewSession(ctx, c)
|
||||
return subService.CreateDocPreviewSession(ctx, c, false)
|
||||
}
|
||||
|
||||
// List 列出分享的目录下的对象
|
||||
|
||||
@@ -3,12 +3,14 @@ package user
|
||||
import (
|
||||
"fmt"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/email"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"net/url"
|
||||
)
|
||||
@@ -154,3 +156,48 @@ func (service *UserLoginService) Login(c *gin.Context) serializer.Response {
|
||||
return serializer.BuildUserResponse(expectedUser)
|
||||
|
||||
}
|
||||
|
||||
// CopySessionService service for copy user session
|
||||
type CopySessionService struct {
|
||||
ID string `uri:"id" binding:"required,uuid4"`
|
||||
}
|
||||
|
||||
const CopySessionTTL = 60
|
||||
|
||||
// Prepare generates the URL with short expiration duration
|
||||
func (s *CopySessionService) Prepare(c *gin.Context, user *model.User) serializer.Response {
|
||||
// 用户组有效期
|
||||
urlID := uuid.Must(uuid.NewV4())
|
||||
if err := cache.Set(fmt.Sprintf("copy_session_%s", urlID.String()), user.ID, CopySessionTTL); err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to create copy session", err)
|
||||
}
|
||||
|
||||
base := model.GetSiteURL()
|
||||
apiBaseURI, _ := url.Parse("/api/v3/user/session/copy/" + urlID.String())
|
||||
apiURL := base.ResolveReference(apiBaseURI)
|
||||
res, err := auth.SignURI(auth.General, apiURL.String(), CopySessionTTL)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to sign temp URL", err)
|
||||
}
|
||||
|
||||
return serializer.Response{
|
||||
Data: res.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// Copy a new session from active session, refresh max-age
|
||||
func (s *CopySessionService) Copy(c *gin.Context) serializer.Response {
|
||||
// 用户组有效期
|
||||
cacheKey := fmt.Sprintf("copy_session_%s", s.ID)
|
||||
uid, ok := cache.Get(cacheKey)
|
||||
if !ok {
|
||||
return serializer.Err(serializer.CodeNotFound, "", nil)
|
||||
}
|
||||
|
||||
cache.Deletes([]string{cacheKey}, "")
|
||||
util.SetSession(c, map[string]interface{}{
|
||||
"user_id": uid.(uint),
|
||||
})
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
@@ -76,8 +76,6 @@ type ThemeChose struct {
|
||||
Theme string `json:"theme" binding:"required,hexcolor|rgb|rgba|hsl"`
|
||||
}
|
||||
|
||||
const avatarMaxAge = 3600
|
||||
|
||||
// Update 更新主题设定
|
||||
func (service *ThemeChose) Update(c *gin.Context, user *model.User) serializer.Response {
|
||||
user.OptionsSerialized.PreferredTheme = service.Theme
|
||||
@@ -196,8 +194,6 @@ func (service *AvatarService) Get(c *gin.Context) serializer.Response {
|
||||
"l": model.GetSettingByName("avatar_size_l"),
|
||||
}
|
||||
|
||||
c.Header("Cache-Control", fmt.Sprintf("max-age=%d", avatarMaxAge))
|
||||
|
||||
// Gravatar 头像重定向
|
||||
if user.Avatar == "gravatar" {
|
||||
server := model.GetSettingByName("gravatar_server")
|
||||
|
||||
Reference in New Issue
Block a user