Compare commits

..
90 Commits
Author SHA1 Message Date
Aaron Liu 88409cc1f0 release: 3.8.3 2023-10-07 20:08:38 +08:00
Darren YuandGitHub cd6eee0b60 Fix: doc preview src of local storage policy starts with ":/" (#1842) (#1844)
* Fix: doc preview src of local storage policy starts with ":/"

* Fix: doc preview src of local storage policy starts with ":/"
2023-10-06 12:34:49 +08:00
SamandGitHub 3ffce1e356 fix(admin): Able to change deafult user status (#1811) 2023-08-14 13:24:58 +08:00
Aaron Liu ce832bf13d release: 3.8.2 2023-08-07 20:09:23 +08:00
Aaron Liu 5642dd3b66 feat(webdav): support setting download proxy 2023-07-29 08:58:14 +08:00
Aaron Liu a1747073df feat(webdav): support setting download proxy 2023-07-29 08:53:26 +08:00
WeidiDengandGitHub ad6c6bcd93 feat(webdav): supoort rename in copy and move (#1774) 2023-07-18 15:27:56 +08:00
WeidiDengandGitHub f4a04ce3c3 fix webdav proppatch (#1771) 2023-07-18 15:25:43 +08:00
Aaron Liu 247e31079c fix(thumb): cannot generate thumb using ffmpeg for specific format (#1756) 2023-07-18 15:18:54 +08:00
Darren YuandGitHub a26893aabc Add: thumb quality for 3rd storage policy (#1763)
Add thumb quality for third party storage policy.
2023-07-05 22:13:24 +08:00
初雪andGitHub ce759c02b1 feat(redis): support confiuring username (#1752)
替换Golang Redis依赖: redigo的版本至当前最新版1.8.9
(v2.0.0被标记为已撤回,且长期未更新)

Redis 6 及以上版本均可配置为使用username+password认证的ACL,故作此变更。
2023-07-05 22:12:33 +08:00
Aaron Liu 9f6f9adc89 Merge remote-tracking branch 'origin/master' 2023-06-25 18:53:54 +08:00
Aaron Liu 91025b9f24 fix(thumb): cannot generate thumbnails in slave mode 2023-06-25 18:53:37 +08:00
hallucinationandGitHub a9bee3e638 Update docker-compose.yml (#1727)
add  Redis retains login sessions after restarting
2023-06-19 12:38:29 +08:00
Aaron Liu 243c312066 fix: failed UT 2023-06-11 09:50:57 +08:00
Aaron Liu 1d52ddd93a release: 3.8.0 2023-06-11 09:45:27 +08:00
Aaron Liu cbc549229b fix(wopi): anonymous users cannot preview files 2023-06-11 09:45:06 +08:00
Aaron Liu 173ca6cdf8 fix(preview): use absolute URL for local storage policy 2023-06-11 09:44:43 +08:00
Aaron Liu fb166fb3e4 release: 3.8.0-beta1 2023-05-27 14:06:19 +08:00
Aaron Liu b1344616b8 test: fix failed ut 2023-05-27 10:44:28 +08:00
Aaron Liu 89ee147961 feat(upload): detect and specify mime type for files uploaded to S3 and OSS (fix#1681) 2023-05-25 19:51:51 +08:00
Aaron Liu 4aafe1dc7a enhance(download): Use just-in-time host in download URl, instead of SiteURL in site settings 2023-05-25 19:49:32 +08:00
Aaron Liu 4c834e75fa adhoc: commit todo changes related to google drive 2023-05-25 19:46:05 +08:00
Aaron Liu 31d4a3445d fix(cache): panic if redis connection fails 2023-05-25 19:44:59 +08:00
Aaron Liu 37926e3133 feat(policy): add Google Drive Oauth client 2023-05-24 14:39:54 +08:00
WeidiDengandGitHub 4c18e5acd1 webdav兼容nextcloud propset设置修改时间 (#1710) 2023-05-24 12:10:03 +08:00
ArkylinandGitHub 6358740cc9 modified: models/policy.go (#1718)
modified:   models/policy_test.go
2023-05-24 12:09:24 +08:00
Aaron Liu 00d56d6d07 test: fix failed ut 2023-04-16 09:25:57 +08:00
Aaron Liu b9143b53f6 chore: update runner to ubuntu-latest 2023-04-16 09:20:30 +08:00
Aaron Liu b9d9e036c9 feat(kv): persist cache and session into disk before shutdown 2023-04-16 09:17:06 +08:00
Aaron Liu 4d131db504 test(hook): NewWebdavAfterUploadHook 2023-04-15 09:21:29 +08:00
Aaron Liu c5ffdbfcfb Merge remote-tracking branch 'origin/master' 2023-04-13 19:39:22 +08:00
Aaron Liu 8e2fc1a8f6 test(thumb): new changes in filesystem pkg 2023-04-13 19:39:12 +08:00
AaronLiuandGitHub ce579d387a Merge pull request #1690 from cloudreve/webdav-checksum
webdav兼容rclone的nextcloud选项(修改日期和checksum)
2023-04-08 10:10:46 +08:00
AaronLiuandGitHub f1e7af67bc Merge branch 'master' into webdav-checksum 2023-04-08 10:09:55 +08:00
AaronLiuandGitHub 98788dc72b Merge pull request #1679 from xkeyC/master
feat(Webdav): Add overwrite support for moveFiles and copyFiles
2023-04-08 10:08:13 +08:00
Weidi Deng 1b4eff624d webdav兼容rclone的nextcloud选项(修改日期和checksum) 2023-04-07 22:16:11 +08:00
Aaron Liu 408733a974 test(thumb): new changes in models/cache pkg 2023-04-07 20:33:05 +08:00
Aaron Liu c8b736bd8f fix(dashboard): add missing utils for thumb setting 2023-04-07 19:42:23 +08:00
Aaron Liu cf03206283 feat(thumb): generator settings and test button 2023-04-07 19:33:02 +08:00
Aaron Liu ac536408c6 feat(thumb): use libreoffice to generate thumb 2023-04-07 19:31:43 +08:00
Aaron Liu 98b86b37de feat(thumb): use ffmpeg to generate thumb 2023-04-07 19:30:41 +08:00
Aaron Liu b55344459d feat(thumb): use libvips to generate thumb 2023-04-07 19:30:10 +08:00
Aaron Liu bde4459519 feat(thumb): add ext whitelist for all policy types 2023-04-07 19:29:43 +08:00
Aaron Liu f5a21a7e6f feat(thumb): set size limit for original file 2023-04-07 19:28:39 +08:00
Aaron Liu b910254cc5 feat(thumb): delete generated thumb file
fix(s3): return empty list of file failed to be deleted
2023-04-07 19:27:57 +08:00
Aaron Liu e115497dfe feat(thumb): generate thumb for OneDrive files 2023-04-07 19:27:31 +08:00
Aaron Liu 62b73b577b feat(thumb): generate and return sidecar thumb 2023-04-07 19:26:39 +08:00
Aaron Liu 7cb5e68b78 refactor(thumb): thumb logic for slave policy 2023-04-07 19:25:29 +08:00
Aaron Liu ae118c337e refactor(thumb): reset thumb status after renaming a file with no thumb available 2023-04-07 19:09:13 +08:00
Aaron Liu f36e39991d refactor(thumb): new thumb pipeline model to generate thumb on-demand 2023-04-07 19:08:54 +08:00
Aaron Liu da1eaf2d1f fix(wopi): cannot set preferred language for LibreOffice online 2023-04-07 19:06:46 +08:00
xkeyC 42f7613bfa moveFiles 修改回无条件 overwrite (Move 或 Rename 都会处罚冲突问题) 2023-03-29 20:16:09 +08:00
xkeyC e8e38029ca fix:error code 2023-03-28 00:19:18 +08:00
xkeyC cd9e9e25b9 fix:仅在需要移动时 overwrite 2023-03-28 00:06:10 +08:00
xkeyC ca7b21dc3e feat(Webdav):Add overwrite support for moveFiles and copyFiles 2023-03-27 22:55:20 +08:00
Aaron Liu f172220825 release: 3.7.1 2023-02-13 19:28:00 +08:00
Aaron Liu 37cb292530 fix(db): SQLite3 dialects return empty rows in HasColumn method 2023-02-12 20:02:35 +08:00
Aaron Liu 835605a5cb chore: keep artifacts naming consistent 2023-02-10 13:02:37 +08:00
Aaron Liu 35c4215c0f chore: update archive config to keep original arch name 2023-02-10 12:13:14 +08:00
Aaron Liu 3db803ed38 chore: update readme and archive config 2023-02-09 20:29:38 +08:00
Aaron Liu c2d7168c26 release: 3.7.0 2023-02-09 19:03:52 +08:00
Aaron Liu b441d884f6 chore: fix amd64.v1 inconsistency 2023-02-08 22:10:58 +08:00
Aaron Liu d4c79cb962 chore: fix amd64.v1 inconsistency 2023-02-08 21:39:04 +08:00
Aaron Liu e134826bd1 chore: skip git validation before release 2023-02-08 21:10:55 +08:00
Aaron Liu b78f475df8 chore: use goreleaser to build docker images 2023-02-08 21:06:14 +08:00
Aaron Liu e7de7e868d chore: use goreleaser to build artifacts 2023-02-08 20:04:45 +08:00
Aaron Liu a58e3b19ec Revert "chore(build): add go-task support (#1608)"
This reverts commit abe90e4c88.

Revert "chore: fix env in task yaml and test new build action"

This reverts commit 7dfe8fb439.

Revert "remove unused env"

This reverts commit 076aa2c567.

Revert "fix: ci build failed as env in go tasks cannot be overwritten"

This reverts commit 71cc332109.
2023-02-08 15:42:42 +08:00
Aaron Liu 71cc332109 fix: ci build failed as env in go tasks cannot be overwritten 2023-02-08 15:32:34 +08:00
Aaron Liu 076aa2c567 remove unused env 2023-02-08 15:11:53 +08:00
Aaron Liu 7dfe8fb439 chore: fix env in task yaml and test new build action 2023-02-08 15:08:20 +08:00
Aaron Liu b1b74b7be5 Merge remote-tracking branch 'origin/master' 2023-02-08 13:57:54 +08:00
mritdandGitHub abe90e4c88 chore(build): add go-task support (#1608)
* chore(build): add go-task support

add go-task support

Signed-off-by: kovacs <mritd@linux.com>

* chore(docker): build with go-task

build with go-task

Signed-off-by: kovacs <mritd@linux.com>

* chore(task): support cross compile

support cross compile

Signed-off-by: kovacs <mritd@linux.com>

* chore(task): remove GCC build

remove GCC build

Signed-off-by: kovacs <mritd@linux.com>

* docs(task): update README

update README

Signed-off-by: kovacs <mritd@linux.com>

---------

Signed-off-by: kovacs <mritd@linux.com>
2023-02-08 13:57:21 +08:00
Aaron Liu 95027e4f5d refactor(db): move dialects to a standalone pkg 2023-02-08 10:06:24 +08:00
VigorFoxandGitHub 9c58278e08 refactor(db): change SQLite driver from github.com/jinzhu/gorm/dialects/sqlite to github.com/glebarez/go-sqlite (#1626)
* sqlite 驱动从 github.com/jinzhu/gorm/dialects/sqlite 改为 github.com/glebarez/go-sqlite,以移除对 cgo 的依赖

* // 兼容已有配置中的 "sqlite3" 配置项

* Update models/init.go: 修改变量名
2023-02-08 09:53:41 +08:00
Aaron Liu 6d1c44f21b test: fix failed ut 2023-02-07 20:24:21 +08:00
Aaron Liu 489a2bab4f test: delete file while user not found 2023-02-07 20:18:13 +08:00
Aaron Liu d67d0512f8 feat(explorer): advance delete options for users 2023-02-07 20:08:22 +08:00
Aaron Liu 1c1cd9b342 feat(dashboard): unlink file while not deleting its physical source (#789) 2023-02-07 20:07:05 +08:00
Aaron Liu 2a1e82aede fix(fs): cannot delete file while user is deleted (fix #1586) 2023-02-07 20:04:53 +08:00
WeidiDengandGitHub a93ea2cfa0 feat(webdav): add read-only option (#1629) 2023-02-07 19:43:28 +08:00
HFO4 ffbafca994 Merge remote-tracking branch 'origin/master' 2023-01-10 19:56:59 +08:00
HFO4 99434d7aa5 test(wopi): add tests for wopi client 2023-01-10 19:56:02 +08:00
HFO4 f7fdf10d70 feat(wopi): edit WOPI related settings 2023-01-09 19:38:55 +08:00
HFO4 9ad2c3508f enhancement(upload): keep original file content after failed to update document files 2023-01-09 19:38:31 +08:00
HFO4 5a8c86c72e feat(wopi): adapt libreoffice online 2023-01-09 19:38:12 +08:00
HFO4 1c922ac981 feat(wopi): implement required rest api as a WOPI host 2023-01-09 19:37:46 +08:00
HFO4 4541400755 feat(wopi): change doc preview config based on WOPI discovery results 2023-01-09 19:36:41 +08:00
HFO4 c39daeb0d0 feat(wopi): fetch discover endpoint 2023-01-09 19:34:39 +08:00
5aaee9andGitHub 8dafb4f40a feat: support connect to mysql with unix socket (#1571) 2022-12-19 19:23:47 +08:00
131 changed files with 4734 additions and 1284 deletions
+10 -34
View File
@@ -5,12 +5,12 @@ on: workflow_dispatch
jobs:
build:
name: Build
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
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 }}
+3 -3
View File
@@ -10,12 +10,12 @@ on:
jobs:
test:
name: Test
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
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
+2
View File
@@ -29,3 +29,5 @@ version.lock
conf/conf.ini
/statik/
.vscode/
dist/
+121
View File
@@ -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
View File
@@ -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
+1 -33
View File
@@ -1,39 +1,7 @@
# the frontend builder
# cloudreve need node.js 16* to build frontend,
# separate build step and custom image tag will resolve this
FROM node:16-alpine as cloudreve_frontend_builder
RUN apk update \
&& apk add --no-cache wget curl git yarn zip bash \
&& git clone --recurse-submodules https://github.com/cloudreve/Cloudreve.git /cloudreve_frontend
# build frontend assets using build script, make sure all the steps just follow the regular release
WORKDIR /cloudreve_frontend
ENV GENERATE_SOURCEMAP false
RUN chmod +x ./build.sh && ./build.sh -a
# the backend builder
# cloudreve backend needs golang 1.18* to build
FROM golang:1.18-alpine as cloudreve_backend_builder
# install dependencies and build tools
RUN apk update \
# install dependencies and build tools
&& apk add --no-cache wget curl git build-base gcc abuild binutils binutils-doc gcc-doc zip bash \
&& git clone --recurse-submodules https://github.com/cloudreve/Cloudreve.git /cloudreve_backend
WORKDIR /cloudreve_backend
COPY --from=cloudreve_frontend_builder /cloudreve_frontend/assets.zip ./
RUN chmod +x ./build.sh && ./build.sh -c
# TODO: merge the frontend build and backend build into a single one image
# the final published image
FROM alpine:latest
WORKDIR /cloudreve
COPY --from=cloudreve_backend_builder /cloudreve_backend/cloudreve ./cloudreve
COPY cloudreve ./cloudreve
RUN apk update \
&& apk add --no-cache tzdata \
+9 -35
View File
@@ -50,7 +50,7 @@
* :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, texts, Office documents, ePub files online.
* :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.
* 🌈 ... ...
@@ -74,7 +74,13 @@ The above is a minimum deploy example, you can refer to [Getting started](https:
## :gear: Build
You need to have `Go >= 1.18`, `node.js`, `yarn`, `zip` and other necessary dependencies before you can build it yourself.
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
@@ -82,42 +88,10 @@ You need to have `Go >= 1.18`, `node.js`, `yarn`, `zip` and other necessary depe
git clone --recurse-submodules https://github.com/cloudreve/Cloudreve.git
```
#### Build static resources
```shell
# Enter frontend sub-module
cd assets
# Install dependencies
yarn install
# Start building
yarn run build
# Delete unused map files
cd build
find . -name "*.map" -type f -delete
# Return to main folder to pack static files
cd ../../
zip -r - assets/build >assets.zip
```
#### Compile
```shell
# Obtain version number, commit SHA
export COMMIT_SHA=$(git rev-parse --short HEAD)
export VERSION=$(git describe --tags)
# Compile
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'"
```
You can also start a quick build using `build.sh` in the project root directory:
```shell
./build.sh [-a] [-c] [-b] [-r]
a - Build assets
c - Build binary backend
b - Build both assets and backend
r - Cross-compilation for final release
goreleaser build --clean --single-target --snapshot
```
## :alembic: Stacks
+9 -35
View File
@@ -50,7 +50,7 @@
* :card_file_box: 文件拖拽管理
* :family_woman_girl_boy: 多用户、用户组、多存储策略
* :link: 创建文件、目录的分享链接,可设定自动过期
* :eye_speech_bubble: 视频、图像、音频、文本、Office 文档、 ePub 在线预览
* :eye_speech_bubble: 视频、图像、音频、 ePub 在线预览,文本、Office 文档在线编辑
* :art: 自定义配色、黑暗模式、PWA 应用、全站单页应用、国际化支持
* :rocket: All-In-One 打包,开箱即用
* 🌈 ... ...
@@ -74,7 +74,13 @@ chmod +x ./cloudreve
## :gear: 构建
自行构建前需要拥有 `Go >= 1.18``node.js``yarn``zip` 等必要依赖。
自行构建前需要拥有 `Go >= 1.18``node.js``yarn``zip`, [goreleaser](https://goreleaser.com/intro/) 等必要依赖。
#### 安装 goreleaser
```shell
go install github.com/goreleaser/goreleaser@latest
```
#### 克隆代码
@@ -82,42 +88,10 @@ chmod +x ./cloudreve
git clone --recurse-submodules https://github.com/cloudreve/Cloudreve.git
```
#### 构建静态资源
```shell
# 进入前端子模块
cd assets
# 安装依赖
yarn install
# 开始构建
yarn run build
# 构建完成后删除映射文件
cd build
find . -name "*.map" -type f -delete
# 返回项目主目录打包静态资源
cd ../../
zip -r - assets/build >assets.zip
```
#### 编译项目
```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的版本
goreleaser build --clean --single-target --snapshot
```
## :alembic: 技术栈
+1 -1
Submodule assets updated: 01343d7656...5d4d01a797
+27 -1
View File
@@ -12,8 +12,10 @@ 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"
"path/filepath"
)
// Init 初始化启动
@@ -38,7 +40,19 @@ func Init(path string, statics fs.FS) {
{
"both",
func() {
cache.Init(conf.SystemConfig.Mode == "slave")
cache.Init()
},
},
{
"slave",
func() {
model.InitSlaveDefaults()
},
},
{
"slave",
func() {
cache.InitSlaveOverwrites()
},
},
{
@@ -47,6 +61,12 @@ func Init(path string, statics fs.FS) {
model.Init()
},
},
{
"both",
func() {
cache.Restore(filepath.Join(model.GetSettingByName("temp_path"), cache.DefaultCacheFile))
},
},
{
"both",
func() {
@@ -95,6 +115,12 @@ func Init(path string, statics fs.FS) {
auth.Init()
},
},
{
"master",
func() {
wopi.Init()
},
},
}
for _, dependency := range dependencies {
-136
View File
@@ -1,136 +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 --network-timeout 1000000
yarn install
yarn run build
cd build
cd $REPO
# please keep in mind that if this final output binary `assets.zip` name changed, please go and update the `Dockerfile` as well
zip -r - assets/build >assets.zip
}
buildBinary() {
cd $REPO
# same as assets, if this final output binary `cloudreve` name changed, please go and update the `Dockerfile`
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
+13 -1
View File
@@ -1,5 +1,14 @@
version: "3.8"
services:
redis:
container_name: redis
image: bitnami/redis:latest
restart: unless-stopped
environment:
- ALLOW_EMPTY_PASSWORD=yes
volumes:
- redis_data:/bitnami/redis/data
cloudreve:
container_name: cloudreve
image: cloudreve/cloudreve:latest
@@ -14,6 +23,7 @@ services:
- ./cloudreve/avatar:/cloudreve/avatar
depends_on:
- aria2
aria2:
container_name: aria2
image: p3terx/aria2-pro # third party image, please keep notice what you are doing
@@ -25,9 +35,11 @@ services:
- ./aria2/config:/config
- temp_data:/data
volumes:
redis_data:
driver: local
temp_data:
driver: local
driver_opts:
type: none
device: $PWD/data
o: bind
o: bind
+21 -11
View File
@@ -13,12 +13,15 @@ require (
github.com/gin-contrib/sessions v0.0.5
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2
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.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
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.1
github.com/gorilla/websocket v1.4.2
github.com/hashicorp/go-version v1.3.0
github.com/jinzhu/gorm v1.9.11
@@ -30,6 +33,7 @@ require (
github.com/qiniu/go-sdk/v7 v7.11.1
github.com/rafaeljusto/redigomock v0.0.0-20191117212112-00b2509252a1
github.com/robfig/cron/v3 v3.0.1
github.com/samber/lo v1.38.1
github.com/speps/go-hashids v2.0.0+incompatible
github.com/stretchr/testify v1.7.2
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/captcha v1.0.393
@@ -39,6 +43,7 @@ require (
github.com/upyun/go-sdk v2.1.0+incompatible
golang.org/x/image v0.0.0-20211028202545-6944b10bf410
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
google.golang.org/api v0.45.0
)
require (
@@ -47,7 +52,6 @@ require (
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/speakeasy v0.1.0 // indirect
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
@@ -59,7 +63,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
@@ -79,11 +83,10 @@ 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/googleapis/gax-go/v2 v2.0.5 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // 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
@@ -98,9 +101,8 @@ require (
github.com/leodido/go-urn v1.2.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.14 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.12 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // 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
@@ -115,7 +117,7 @@ require (
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-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
@@ -141,17 +143,19 @@ require (
go.etcd.io/etcd/server/v3 v3.5.0-alpha.0 // indirect
go.etcd.io/etcd/tests/v3 v3.5.0-alpha.0 // indirect
go.etcd.io/etcd/v3 v3.5.0-alpha.0 // indirect
go.opencensus.io v0.23.0 // indirect
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-20220622213112-05595931fe9d // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 // 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-20220702020025-31831981b65f // 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/tools v0.1.8-0.20211029000441-d6a9af8af023 // 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
@@ -162,6 +166,12 @@ require (
gopkg.in/mail.v2 v2.3.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // 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
)
replace github.com/gomodule/redigo v2.0.0+incompatible => github.com/gomodule/redigo v1.8.9
+36 -16
View File
@@ -131,8 +131,6 @@ github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQ
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04=
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/caarlos0/ctrlc v1.0.0/go.mod h1:CdXpj4rmq0q/1Eb44M9zi2nKB0QraNKuRGYGrrHhcQw=
@@ -211,8 +209,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=
@@ -269,6 +268,8 @@ github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
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/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=
@@ -361,8 +362,8 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
@@ -381,8 +382,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=
@@ -406,6 +408,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=
@@ -421,6 +424,7 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs16741s=
github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@@ -433,7 +437,6 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
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.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=
@@ -590,8 +593,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.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
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=
@@ -602,7 +605,6 @@ github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsO
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
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-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
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=
@@ -746,11 +748,12 @@ 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-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=
@@ -769,6 +772,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
@@ -928,6 +933,7 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@@ -981,6 +987,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -1009,8 +1017,9 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 h1:LQmS1nU0twXLA96Kt7U9qtHJEbBk3z6Q0V4UXjZkpr4=
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1160,8 +1169,9 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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-20220702020025-31831981b65f h1:xdsejrW/0Wf2diT5CPp3XmKUNbr7Xvw8kYilQ+6qjRY=
golang.org/x/sys v0.0.0-20220702020025-31831981b65f/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=
@@ -1244,8 +1254,9 @@ golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023 h1:0c3L82FDQ5rt1bjTBlchS8t6RQ6299/+5bWMnRLh+uI=
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1276,6 +1287,7 @@ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34q
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.45.0 h1:pqMffJFLBVUDIoYsHcqtxgQVTsmxMDpYLOc5MT4Jrww=
google.golang.org/api v0.45.0/go.mod h1:ISLIJCedJolbZvDfAk+Ctuq5hf+aJ33WgtUsfyFoLXA=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -1434,6 +1446,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=
+29 -13
View File
@@ -9,11 +9,13 @@ import (
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"github.com/cloudreve/Cloudreve/v3/bootstrap"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/cloudreve/Cloudreve/v3/routers"
@@ -67,20 +69,10 @@ func main() {
// 收到信号后关闭服务器
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
go func() {
sig := <-sigChan
util.Log().Info("Signal %s received, shutting down server...", sig)
ctx := context.Background()
if conf.SystemConfig.GracePeriod != 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Duration(conf.SystemConfig.GracePeriod)*time.Second)
defer cancel()
}
go shutdown(sigChan, server)
err := server.Shutdown(ctx)
if err != nil {
util.Log().Error("Failed to shutdown server: %s", err)
}
defer func() {
<-sigChan
}()
// 如果启用了SSL
@@ -140,3 +132,27 @@ func RunUnix(server *http.Server) error {
return server.Serve(listener)
}
func shutdown(sigChan chan os.Signal, server *http.Server) {
sig := <-sigChan
util.Log().Info("Signal %s received, shutting down server...", sig)
ctx := context.Background()
if conf.SystemConfig.GracePeriod != 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Duration(conf.SystemConfig.GracePeriod)*time.Second)
defer cancel()
}
// Shutdown http server
err := server.Shutdown(ctx)
if err != nil {
util.Log().Error("Failed to shutdown server: %s", err)
}
// Persist in-memory cache
if err := cache.Store.Persist(filepath.Join(model.GetSettingByName("temp_path"), cache.DefaultCacheFile)); err != nil {
util.Log().Warning("Failed to persist cache: %s", err)
}
close(sigChan)
}
+5
View File
@@ -116,6 +116,11 @@ func WebDAVAuth() gin.HandlerFunc {
return
}
// 用户组已启用WebDAV代理?
if !expectedUser.Group.OptionsSerialized.WebDAVProxy {
webdav.UseProxy = false
}
c.Set("user", &expectedUser)
c.Set("webdav", webdav)
c.Next()
+4 -14
View File
@@ -1,6 +1,8 @@
package middleware
import (
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/sessionstore"
"net/http"
"strings"
@@ -8,28 +10,16 @@ import (
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/memstore"
"github.com/gin-contrib/sessions/redis"
"github.com/gin-gonic/gin"
)
// Store session存储
var Store memstore.Store
var Store sessions.Store
// Session 初始化session
func Session(secret string) gin.HandlerFunc {
// Redis设置不为空,且非测试模式时使用Redis
if conf.RedisConfig.Server != "" && gin.Mode() != gin.TestMode {
var err error
Store, err = redis.NewStoreWithDB(10, conf.RedisConfig.Network, conf.RedisConfig.Server, conf.RedisConfig.Password, conf.RedisConfig.DB, []byte(secret))
if err != nil {
util.Log().Panic("Failed to connect to Redis%s", err)
}
util.Log().Info("Connect to Redis server %q.", conf.RedisConfig.Server)
} else {
Store = memstore.NewStore([]byte(secret))
}
Store = sessionstore.NewStore(cache.Store, []byte(secret))
sameSiteMode := http.SameSiteDefaultMode
switch strings.ToLower(conf.CORSConfig.SameSite) {
-9
View File
@@ -5,7 +5,6 @@ import (
"net/http/httptest"
"testing"
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
@@ -20,14 +19,6 @@ func TestSession(t *testing.T) {
asserts.NotNil(Store)
asserts.IsType(emptyFunc(), handler)
}
{
conf.RedisConfig.Server = "123"
asserts.Panics(func() {
Session("2333")
})
conf.RedisConfig.Server = ""
}
}
func emptyFunc() gin.HandlerFunc {
+70
View File
@@ -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()
}
}
+112
View File
@@ -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")
})
}
}
+26 -1
View File
@@ -1,6 +1,7 @@
package model
import (
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/gofrs/uuid"
@@ -25,7 +26,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"},
@@ -106,6 +107,20 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti
{Name: "thumb_encode_method", Value: "jpg", Type: "thumb"},
{Name: "thumb_gc_after_gen", Value: "0", Type: "thumb"},
{Name: "thumb_encode_quality", Value: "85", Type: "thumb"},
{Name: "thumb_builtin_enabled", Value: "1", Type: "thumb"},
{Name: "thumb_vips_enabled", Value: "0", Type: "thumb"},
{Name: "thumb_ffmpeg_enabled", Value: "0", Type: "thumb"},
{Name: "thumb_vips_path", Value: "vips", Type: "thumb"},
{Name: "thumb_vips_exts", Value: "csv,mat,img,hdr,pbm,pgm,ppm,pfm,pnm,svg,svgz,j2k,jp2,jpt,j2c,jpc,gif,png,jpg,jpeg,jpe,webp,tif,tiff,fits,fit,fts,exr,jxl,pdf,heic,heif,avif,svs,vms,vmu,ndpi,scn,mrxs,svslide,bif,raw", Type: "thumb"},
{Name: "thumb_ffmpeg_seek", Value: "00:00:01.00", Type: "thumb"},
{Name: "thumb_ffmpeg_path", Value: "ffmpeg", Type: "thumb"},
{Name: "thumb_ffmpeg_exts", Value: "3g2,3gp,asf,asx,avi,divx,flv,m2ts,m2v,m4v,mkv,mov,mp4,mpeg,mpg,mts,mxf,ogv,rm,swf,webm,wmv", Type: "thumb"},
{Name: "thumb_libreoffice_path", Value: "soffice", Type: "thumb"},
{Name: "thumb_libreoffice_enabled", Value: "0", Type: "thumb"},
{Name: "thumb_libreoffice_exts", Value: "md,ods,ots,fods,uos,xlsx,xml,xls,xlt,dif,dbf,html,slk,csv,xlsm,docx,dotx,doc,dot,rtf,xlsm,xlst,xls,xlw,xlc,xlt,pptx,ppsx,potx,pomx,ppt,pps,ppm,pot,pom", Type: "thumb"},
{Name: "thumb_proxy_enabled", Value: "0", Type: "thumb"},
{Name: "thumb_proxy_policy", Value: "[]", Type: "thumb"},
{Name: "thumb_max_src_size", Value: "31457280", Type: "thumb"},
{Name: "pwa_small_icon", Value: "/static/img/favicon.ico", Type: "pwa"},
{Name: "pwa_medium_icon", Value: "/static/img/logo192.png", Type: "pwa"},
{Name: "pwa_large_icon", Value: "/static/img/logo512.png", Type: "pwa"},
@@ -115,4 +130,14 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti
{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"},
}
func InitSlaveDefaults() {
for _, setting := range defaultSettings {
cache.Set("setting_"+setting.Name, setting.Value, -1)
}
}
+288
View File
@@ -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
}
+99 -10
View File
@@ -6,6 +6,8 @@ import (
"errors"
"fmt"
"path"
"path/filepath"
"strings"
"time"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
@@ -34,6 +36,18 @@ type File struct {
MetadataSerialized map[string]string `gorm:"-"`
}
// Thumb related metadata
const (
ThumbStatusNotExist = ""
ThumbStatusExist = "exist"
ThumbStatusNotAvailable = "not_available"
ThumbStatusMetadataKey = "thumb_status"
ThumbSidecarMetadataKey = "thumb_sidecar"
ChecksumMetadataKey = "webdav_checksum"
)
func init() {
// 注册缓存用到的复杂结构
gob.Register(File{})
@@ -64,6 +78,8 @@ func (file *File) AfterFind() (err error) {
// 反序列化文件元数据
if file.Metadata != "" {
err = json.Unmarshal([]byte(file.Metadata), &file.MetadataSerialized)
} else {
file.MetadataSerialized = make(map[string]string)
}
return
@@ -71,9 +87,13 @@ func (file *File) AfterFind() (err error) {
// BeforeSave Save策略前的钩子
func (file *File) BeforeSave() (err error) {
metaValue, err := json.Marshal(&file.MetadataSerialized)
file.Metadata = string(metaValue)
return err
if len(file.MetadataSerialized) > 0 {
metaValue, err := json.Marshal(&file.MetadataSerialized)
file.Metadata = string(metaValue)
return err
}
return nil
}
// GetChildFile 查找目录下名为name的子文件
@@ -234,7 +254,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 +273,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
@@ -277,7 +299,19 @@ func GetFilesByUploadSession(sessionID string, uid uint) (*File, error) {
// Rename 重命名文件
func (file *File) Rename(new string) error {
return DB.Model(&file).UpdateColumn("name", new).Error
if file.MetadataSerialized[ThumbStatusMetadataKey] == ThumbStatusNotAvailable {
if !strings.EqualFold(filepath.Ext(new), filepath.Ext(file.Name)) {
// Reset thumb status for new ext name.
if err := file.resetThumb(); err != nil {
return err
}
}
}
return DB.Model(&file).Set("gorm:association_autoupdate", false).Updates(map[string]interface{}{
"name": new,
"metadata": file.Metadata,
}).Error
}
// UpdatePicInfo 更新文件的图像信息
@@ -285,6 +319,23 @@ func (file *File) UpdatePicInfo(value string) error {
return DB.Model(&file).Set("gorm:association_autoupdate", false).UpdateColumns(File{PicInfo: value}).Error
}
// UpdateMetadata 新增或修改文件的元信息
func (file *File) UpdateMetadata(data map[string]string) error {
if file.MetadataSerialized == nil {
file.MetadataSerialized = make(map[string]string)
}
for k, v := range data {
file.MetadataSerialized[k] = v
}
metaValue, err := json.Marshal(&file.MetadataSerialized)
if err != nil {
return err
}
return DB.Model(&file).Set("gorm:association_autoupdate", false).UpdateColumns(File{Metadata: string(metaValue)}).Error
}
// UpdateSize 更新文件的大小信息
// TODO: 全局锁
func (file *File) UpdateSize(value uint64) error {
@@ -300,10 +351,18 @@ func (file *File) UpdateSize(value uint64) error {
sizeDelta = file.Size - value
}
if err := file.resetThumb(); err != nil {
tx.Rollback()
return err
}
if res := tx.Model(&file).
Where("size = ?", file.Size).
Set("gorm:association_autoupdate", false).
Update("size", value); res.Error != nil {
Updates(map[string]interface{}{
"size": value,
"metadata": file.Metadata,
}); res.Error != nil {
tx.Rollback()
return res.Error
}
@@ -319,7 +378,14 @@ func (file *File) UpdateSize(value uint64) error {
// UpdateSourceName 更新文件的源文件名
func (file *File) UpdateSourceName(value string) error {
return DB.Model(&file).Set("gorm:association_autoupdate", false).Update("source_name", value).Error
if err := file.resetThumb(); err != nil {
return err
}
return DB.Model(&file).Set("gorm:association_autoupdate", false).Updates(map[string]interface{}{
"source_name": value,
"metadata": file.Metadata,
}).Error
}
func (file *File) PopChunkToFile(lastModified *time.Time, picInfo string) error {
@@ -359,6 +425,17 @@ func (file *File) CreateOrGetSourceLink() (*SourceLink, error) {
return res, nil
}
func (file *File) resetThumb() error {
if _, ok := file.MetadataSerialized[ThumbStatusMetadataKey]; !ok {
return nil
}
delete(file.MetadataSerialized, ThumbStatusMetadataKey)
metaValue, err := json.Marshal(&file.MetadataSerialized)
file.Metadata = string(metaValue)
return err
}
/*
实现 webdav.FileInfo 接口
*/
@@ -381,3 +458,15 @@ func (file *File) IsDir() bool {
func (file *File) GetPosition() string {
return file.Position
}
// ShouldLoadThumb returns if file explorer should try to load thumbnail for this file.
// `True` does not guarantee the load request will success in next step, but the client
// should try to load and fallback to default placeholder in case error returned.
func (file *File) ShouldLoadThumb() bool {
return file.MetadataSerialized[ThumbStatusMetadataKey] != ThumbStatusNotAvailable
}
// return sidecar thumb file name
func (file *File) ThumbFile() string {
return file.SourceName + GetSettingByNameWithDefault("thumb_file_suffix", "._thumb")
}
+160 -29
View File
@@ -52,26 +52,54 @@ func TestFile_Create(t *testing.T) {
func TestFile_AfterFind(t *testing.T) {
a := assert.New(t)
file := File{
Name: "123",
Metadata: "{\"name\":\"123\"}",
// metadata not empty
{
file := File{
Name: "123",
Metadata: "{\"name\":\"123\"}",
}
a.NoError(file.AfterFind())
a.Equal("123", file.MetadataSerialized["name"])
}
a.NoError(file.AfterFind())
a.Equal("123", file.MetadataSerialized["name"])
// metadata empty
{
file := File{
Name: "123",
Metadata: "",
}
a.Nil(file.MetadataSerialized)
a.NoError(file.AfterFind())
a.NotNil(file.MetadataSerialized)
}
}
func TestFile_BeforeSave(t *testing.T) {
a := assert.New(t)
file := File{
Name: "123",
MetadataSerialized: map[string]string{
"name": "123",
},
// metadata not empty
{
file := File{
Name: "123",
MetadataSerialized: map[string]string{
"name": "123",
},
}
a.NoError(file.BeforeSave())
a.Equal("{\"name\":\"123\"}", file.Metadata)
}
a.NoError(file.BeforeSave())
a.Equal("{\"name\":\"123\"}", file.Metadata)
// metadata empty
{
file := File{
Name: "123",
}
a.NoError(file.BeforeSave())
a.Equal("", file.Metadata)
}
}
func TestFolder_GetChildFile(t *testing.T) {
@@ -365,7 +393,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 +403,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 +415,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 +426,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 +439,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)
}
@@ -455,12 +496,45 @@ func TestFile_Updates(t *testing.T) {
// rename
{
mock.ExpectBegin()
mock.ExpectExec("UPDATE(.+)files(.+)SET(.+)").WithArgs("newName", 1).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
err := file.Rename("newName")
asserts.NoError(mock.ExpectationsWereMet())
asserts.NoError(err)
// not reset thumb
{
file := File{Model: gorm.Model{ID: 1}}
mock.ExpectBegin()
mock.ExpectExec("UPDATE(.+)files(.+)SET(.+)").WithArgs("", "newName", sqlmock.AnyArg(), 1).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
err := file.Rename("newName")
asserts.NoError(mock.ExpectationsWereMet())
asserts.NoError(err)
}
// thumb not available, rename base name only
{
file := File{Model: gorm.Model{ID: 1}, Name: "1.txt", MetadataSerialized: map[string]string{
ThumbStatusMetadataKey: ThumbStatusNotAvailable,
},
Metadata: "{}"}
mock.ExpectBegin()
mock.ExpectExec("UPDATE(.+)files(.+)SET(.+)").WithArgs("{}", "newName.txt", sqlmock.AnyArg(), 1).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
err := file.Rename("newName.txt")
asserts.NoError(mock.ExpectationsWereMet())
asserts.NoError(err)
asserts.Equal(ThumbStatusNotAvailable, file.MetadataSerialized[ThumbStatusMetadataKey])
}
// thumb not available, rename base name only
{
file := File{Model: gorm.Model{ID: 1}, Name: "1.txt", MetadataSerialized: map[string]string{
ThumbStatusMetadataKey: ThumbStatusNotAvailable,
}}
mock.ExpectBegin()
mock.ExpectExec("UPDATE(.+)files(.+)SET(.+)").WithArgs("{}", "newName.jpg", sqlmock.AnyArg(), 1).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
err := file.Rename("newName.jpg")
asserts.NoError(mock.ExpectationsWereMet())
asserts.NoError(err)
asserts.Empty(file.MetadataSerialized[ThumbStatusMetadataKey])
}
}
// UpdatePicInfo
@@ -476,7 +550,7 @@ func TestFile_Updates(t *testing.T) {
// UpdateSourceName
{
mock.ExpectBegin()
mock.ExpectExec("UPDATE(.+)").WithArgs("newName", sqlmock.AnyArg(), 1).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("UPDATE(.+)").WithArgs("", "newName", sqlmock.AnyArg(), 1).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
err := file.UpdateSourceName("newName")
asserts.NoError(mock.ExpectationsWereMet())
@@ -491,7 +565,7 @@ func TestFile_UpdateSize(t *testing.T) {
{
file := File{Size: 10}
mock.ExpectBegin()
mock.ExpectExec("UPDATE(.+)files(.+)").WithArgs(11, sqlmock.AnyArg(), 10).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("UPDATE(.+)files(.+)").WithArgs("", 11, sqlmock.AnyArg(), 10).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("UPDATE(.+)storage(.+)+(.+)").WithArgs(uint64(1), sqlmock.AnyArg()).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
@@ -503,7 +577,7 @@ func TestFile_UpdateSize(t *testing.T) {
{
file := File{Size: 10}
mock.ExpectBegin()
mock.ExpectExec("UPDATE(.+)files(.+)").WithArgs(8, sqlmock.AnyArg(), 10).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("UPDATE(.+)files(.+)").WithArgs("", 8, sqlmock.AnyArg(), 10).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("UPDATE(.+)storage(.+)-(.+)").WithArgs(uint64(2), sqlmock.AnyArg()).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
@@ -515,7 +589,7 @@ func TestFile_UpdateSize(t *testing.T) {
{
file := File{Size: 10}
mock.ExpectBegin()
mock.ExpectExec("UPDATE(.+)files(.+)").WithArgs(8, sqlmock.AnyArg(), 10).WillReturnError(errors.New("error"))
mock.ExpectExec("UPDATE(.+)files(.+)").WithArgs("", 8, sqlmock.AnyArg(), 10).WillReturnError(errors.New("error"))
mock.ExpectRollback()
a.Error(file.UpdateSize(8))
@@ -526,7 +600,7 @@ func TestFile_UpdateSize(t *testing.T) {
{
file := File{Size: 10}
mock.ExpectBegin()
mock.ExpectExec("UPDATE(.+)files(.+)").WithArgs(8, sqlmock.AnyArg(), 10).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("UPDATE(.+)files(.+)").WithArgs("", 8, sqlmock.AnyArg(), 10).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("UPDATE(.+)storage(.+)-(.+)").WithArgs(uint64(2), sqlmock.AnyArg()).WillReturnError(errors.New("error"))
mock.ExpectRollback()
@@ -652,3 +726,60 @@ func TestFile_CreateOrGetSourceLink(t *testing.T) {
a.NoError(mock.ExpectationsWereMet())
}
}
func TestFile_UpdateMetadata(t *testing.T) {
a := assert.New(t)
file := &File{}
file.ID = 1
// 更新失败
{
expectedErr := errors.New("error")
mock.ExpectBegin()
mock.ExpectExec("UPDATE(.+)files(.+)").WithArgs(sqlmock.AnyArg(), 1).WillReturnError(expectedErr)
mock.ExpectRollback()
a.ErrorIs(file.UpdateMetadata(map[string]string{"1": "1"}), expectedErr)
a.NoError(mock.ExpectationsWereMet())
}
// 成功
{
mock.ExpectBegin()
mock.ExpectExec("UPDATE(.+)files(.+)").WithArgs(sqlmock.AnyArg(), 1).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
a.NoError(file.UpdateMetadata(map[string]string{"1": "1"}))
a.NoError(mock.ExpectationsWereMet())
a.Equal("1", file.MetadataSerialized["1"])
}
}
func TestFile_ShouldLoadThumb(t *testing.T) {
a := assert.New(t)
file := &File{
MetadataSerialized: map[string]string{},
}
file.ID = 1
// 无缩略图
{
file.MetadataSerialized[ThumbStatusMetadataKey] = ThumbStatusNotAvailable
a.False(file.ShouldLoadThumb())
}
// 有缩略图
{
file.MetadataSerialized[ThumbStatusMetadataKey] = ThumbStatusExist
a.True(file.ShouldLoadThumb())
}
}
func TestFile_ThumbFile(t *testing.T) {
a := assert.New(t)
file := &File{
SourceName: "test",
MetadataSerialized: map[string]string{},
}
file.ID = 1
a.Equal("test._thumb", file.ThumbFile())
}
+29 -7
View File
@@ -18,7 +18,8 @@ type Folder struct {
OwnerID uint `gorm:"index:owner_id"`
// 数据库忽略字段
Position string `gorm:"-"`
Position string `gorm:"-"`
WebdavDstName string `gorm:"-"`
}
// Create 创建目录
@@ -169,6 +170,11 @@ func (folder *Folder) MoveOrCopyFileTo(files []uint, dstFolder *Folder, isCopy b
oldFile.FolderID = dstFolder.ID
oldFile.UserID = dstFolder.OwnerID
// webdav目标名重置
if dstFolder.WebdavDstName != "" {
oldFile.Name = dstFolder.WebdavDstName
}
if err := DB.Create(&oldFile).Error; err != nil {
return copiedSize, err
}
@@ -177,6 +183,14 @@ func (folder *Folder) MoveOrCopyFileTo(files []uint, dstFolder *Folder, isCopy b
}
} else {
var updates = map[string]interface{}{
"folder_id": dstFolder.ID,
}
// webdav目标名重置
if dstFolder.WebdavDstName != "" {
updates["name"] = dstFolder.WebdavDstName
}
// 更改顶级要移动文件的父目录指向
err := DB.Model(File{}).Where(
"id in (?) and user_id = ? and folder_id = ?",
@@ -184,9 +198,7 @@ func (folder *Folder) MoveOrCopyFileTo(files []uint, dstFolder *Folder, isCopy b
folder.OwnerID,
folder.ID,
).
Update(map[string]interface{}{
"folder_id": dstFolder.ID,
}).
Update(updates).
Error
if err != nil {
return 0, err
@@ -221,6 +233,10 @@ func (folder *Folder) CopyFolderTo(folderID uint, dstFolder *Folder) (size uint6
// 顶级目录直接指向新的目的目录
if folder.ID == folderID {
newID = dstFolder.ID
// webdav目标名重置
if dstFolder.WebdavDstName != "" {
folder.Name = dstFolder.WebdavDstName
}
} else if IDCache, ok := newIDCache[*folder.ParentID]; ok {
newID = IDCache
} else {
@@ -282,15 +298,21 @@ func (folder *Folder) MoveFolderTo(dirs []uint, dstFolder *Folder) error {
return errors.New("cannot move a folder into itself")
}
var updates = map[string]interface{}{
"parent_id": dstFolder.ID,
}
// webdav目标名重置
if dstFolder.WebdavDstName != "" {
updates["name"] = dstFolder.WebdavDstName
}
// 更改顶级要移动目录的父目录指向
err := DB.Model(Folder{}).Where(
"id in (?) and owner_id = ? and parent_id = ?",
dirs,
folder.OwnerID,
folder.ID,
).Update(map[string]interface{}{
"parent_id": dstFolder.ID,
}).Error
).Update(updates).Error
return err
+1 -1
View File
@@ -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)
// 测试目录结构
+2
View File
@@ -34,6 +34,8 @@ 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"`
WebDAVProxy bool `json:"webdav_proxy,omitempty"`
}
// GetGroupByID 用ID获取用户组
+30 -14
View File
@@ -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)
+1
View File
@@ -111,6 +111,7 @@ func addDefaultGroups() {
SourceBatchSize: 1000,
Aria2BatchSize: 50,
RedirectedSource: true,
AdvanceDelete: true,
},
}
if err := DB.Create(&defaultAdminGroup).Error; err != nil {
+2 -2
View File
@@ -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()
+33 -41
View File
@@ -4,6 +4,7 @@ import (
"encoding/gob"
"encoding/json"
"github.com/gofrs/uuid"
"github.com/samber/lo"
"path"
"path/filepath"
"strconv"
@@ -47,8 +48,8 @@ type PolicyOption struct {
FileType []string `json:"file_type"`
// MimeType
MimeType string `json:"mimetype"`
// OdRedirect Onedrive 重定向地址
OdRedirect string `json:"od_redirect,omitempty"`
// OauthRedirect Oauth 重定向地址
OauthRedirect string `json:"od_redirect,omitempty"`
// OdProxy Onedrive 反代地址
OdProxy string `json:"od_proxy,omitempty"`
// OdDriver OneDrive 驱动器定位符
@@ -68,18 +69,8 @@ type PolicyOption struct {
// 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 支持缩略图处理的文件扩展名
var thumbSuffix = map[string][]string{
"local": {},
"qiniu": {".psd", ".jpg", ".jpeg", ".png", ".gif", ".webp", ".tiff", ".bmp"},
"oss": {".jpg", ".jpeg", ".png", ".gif", ".webp", ".tiff", ".bmp"},
"cos": {".jpg", ".jpeg", ".png", ".gif", ".webp", ".tiff", ".bmp"},
"upyun": {".svg", ".jpg", ".jpeg", ".png", ".gif", ".webp", ".tiff", ".bmp"},
"s3": {},
"remote": {},
"onedrive": {"*"},
// File extensions that support thumbnail generation using native policy API.
ThumbExts []string `json:"thumb_exts,omitempty"`
}
func init() {
@@ -165,22 +156,23 @@ func (policy *Policy) GenerateFileName(uid uint, origin string) string {
fileRule := policy.FileNameRule
replaceTable := map[string]string{
"{randomkey16}": util.RandStringRunes(16),
"{randomkey8}": util.RandStringRunes(8),
"{timestamp}": strconv.FormatInt(time.Now().Unix(), 10),
"{timestamp_nano}": strconv.FormatInt(time.Now().UnixNano(), 10),
"{uid}": strconv.Itoa(int(uid)),
"{datetime}": time.Now().Format("20060102150405"),
"{date}": time.Now().Format("20060102"),
"{year}": time.Now().Format("2006"),
"{month}": time.Now().Format("01"),
"{day}": time.Now().Format("02"),
"{hour}": time.Now().Format("15"),
"{minute}": time.Now().Format("04"),
"{second}": time.Now().Format("05"),
"{originname}": origin,
"{ext}": filepath.Ext(origin),
"{uuid}": uuid.Must(uuid.NewV4()).String(),
"{randomkey16}": util.RandStringRunes(16),
"{randomkey8}": util.RandStringRunes(8),
"{timestamp}": strconv.FormatInt(time.Now().Unix(), 10),
"{timestamp_nano}": strconv.FormatInt(time.Now().UnixNano(), 10),
"{uid}": strconv.Itoa(int(uid)),
"{datetime}": time.Now().Format("20060102150405"),
"{date}": time.Now().Format("20060102"),
"{year}": time.Now().Format("2006"),
"{month}": time.Now().Format("01"),
"{day}": time.Now().Format("02"),
"{hour}": time.Now().Format("15"),
"{minute}": time.Now().Format("04"),
"{second}": time.Now().Format("05"),
"{originname}": origin,
"{ext}": filepath.Ext(origin),
"{originname_without_ext}": strings.TrimSuffix(origin, filepath.Ext(origin)),
"{uuid}": uuid.Must(uuid.NewV4()).String(),
}
fileRule = util.Replace(replaceTable, fileRule)
@@ -192,17 +184,6 @@ func (policy *Policy) IsDirectlyPreview() bool {
return policy.Type == "local"
}
// IsThumbExist 给定文件名,返回此存储策略下是否可能存在缩略图
func (policy *Policy) IsThumbExist(name string) bool {
if list, ok := thumbSuffix[policy.Type]; ok {
if len(list) == 1 && list[0] == "*" {
return true
}
return util.ContainsString(list, strings.ToLower(filepath.Ext(name)))
}
return false
}
// IsTransitUpload 返回此策略上传给定size文件时是否需要服务端中转
func (policy *Policy) IsTransitUpload(size uint64) bool {
return policy.Type == "local"
@@ -249,3 +230,14 @@ func (policy *Policy) UpdateAccessKeyAndClearCache(s string) error {
func (policy *Policy) ClearCache() {
cache.Deletes([]string{strconv.FormatUint(uint64(policy.ID), 10)}, "policy_")
}
// CouldProxyThumb return if proxy thumbs is allowed for this policy.
func (policy *Policy) CouldProxyThumb() bool {
if policy.Type == "local" || !IsTrueVal(GetSettingByName("thumb_proxy_enabled")) {
return false
}
allowed := make([]uint, 0)
_ = json.Unmarshal([]byte(GetSettingByName("thumb_proxy_policy")), &allowed)
return lo.Contains[uint](allowed, policy.ID)
}
+44 -54
View File
@@ -25,7 +25,7 @@ func TestGetPolicyByID(t *testing.T) {
asserts.NoError(err)
asserts.NoError(mock.ExpectationsWereMet())
asserts.Equal("默认存储策略", policy.Name)
asserts.Equal("123", policy.OptionsSerialized.OdRedirect)
asserts.Equal("123", policy.OptionsSerialized.OauthRedirect)
rows = sqlmock.NewRows([]string{"name", "type", "options"})
mock.ExpectQuery("^SELECT(.+)").WillReturnRows(rows)
@@ -39,7 +39,7 @@ func TestGetPolicyByID(t *testing.T) {
policy, err := GetPolicyByID(uint(22))
asserts.NoError(err)
asserts.Equal("默认存储策略", policy.Name)
asserts.Equal("123", policy.OptionsSerialized.OdRedirect)
asserts.Equal("123", policy.OptionsSerialized.OauthRedirect)
}
@@ -50,7 +50,7 @@ func TestPolicy_BeforeSave(t *testing.T) {
testPolicy := Policy{
OptionsSerialized: PolicyOption{
OdRedirect: "123",
OauthRedirect: "123",
},
}
expected, _ := json.Marshal(testPolicy.OptionsSerialized)
@@ -134,6 +134,12 @@ func TestPolicy_GenerateFileName(t *testing.T) {
testPolicy.FileNameRule = "123{date}ss{datetime}"
asserts.Len(testPolicy.GenerateFileName(1, "123.txt"), 27)
testPolicy.FileNameRule = "{originname_without_ext}"
asserts.Len(testPolicy.GenerateFileName(1, "123.txt"), 3)
testPolicy.FileNameRule = "{originname_without_ext}_{randomkey8}{ext}"
asserts.Len(testPolicy.GenerateFileName(1, "123.txt"), 16)
// 支持{originname}的策略
testPolicy.Type = "local"
testPolicy.FileNameRule = "123{originname}"
@@ -212,57 +218,6 @@ func TestPolicy_Props(t *testing.T) {
asserts.True(policy.IsUploadPlaceholderWithSize())
}
func TestPolicy_IsThumbExist(t *testing.T) {
asserts := assert.New(t)
testCases := []struct {
name string
expect bool
policy string
}{
{
"1.png",
false,
"unknown",
},
{
"1.png",
false,
"local",
},
{
"1.png",
true,
"cos",
},
{
"1",
false,
"cos",
},
{
"1.txt.png",
true,
"cos",
},
{
"1.png.txt",
false,
"cos",
},
{
"1",
true,
"onedrive",
},
}
for _, testCase := range testCases {
policy := Policy{Type: testCase.policy}
asserts.Equal(testCase.expect, policy.IsThumbExist(testCase.name))
}
}
func TestPolicy_UpdateAccessKeyAndClearCache(t *testing.T) {
a := assert.New(t)
cache.Set("policy_1331", Policy{}, 3600)
@@ -277,3 +232,38 @@ func TestPolicy_UpdateAccessKeyAndClearCache(t *testing.T) {
_, ok := cache.Get("policy_1331")
a.False(ok)
}
func TestPolicy_CouldProxyThumb(t *testing.T) {
a := assert.New(t)
p := &Policy{Type: "local"}
// local policy
{
a.False(p.CouldProxyThumb())
}
// feature not enabled
{
p.Type = "remote"
cache.Set("setting_thumb_proxy_enabled", "0", 0)
a.False(p.CouldProxyThumb())
}
// list not contain current policy
{
p.ID = 2
cache.Set("setting_thumb_proxy_enabled", "1", 0)
cache.Set("setting_thumb_proxy_policy", "[1]", 0)
a.False(p.CouldProxyThumb())
}
// enabled
{
p.ID = 2
cache.Set("setting_thumb_proxy_enabled", "1", 0)
cache.Set("setting_thumb_proxy_policy", "[2]", 0)
a.True(p.CouldProxyThumb())
}
cache.Deletes([]string{"thumb_proxy_enabled", "thumb_proxy_policy"}, "setting_")
}
+7
View File
@@ -11,6 +11,8 @@ 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"` // 是否只读
UseProxy bool `gorm:"type:bool"` // 是否进行反代
}
// Create 创建账户
@@ -39,3 +41,8 @@ func ListWebDAVAccounts(uid uint) []Webdav {
func DeleteWebDAVAccountByID(id, uid uint) {
DB.Where("user_id = ? and id = ?", uid, id).Delete(&Webdav{})
}
// UpdateWebDAVAccountByID 根据账户ID和UID更新账户
func UpdateWebDAVAccountByID(id, uid uint, updates map[string]interface{}) {
DB.Model(&Webdav{Model: gorm.Model{ID: id}, UserID: uid}).Updates(updates)
}
+25 -6
View File
@@ -1,31 +1,44 @@
package cache
import (
"encoding/gob"
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/gin-gonic/gin"
)
func init() {
gob.Register(map[string]itemWithTTL{})
}
// Store 缓存存储器
var Store Driver = NewMemoStore()
// Init 初始化缓存
func Init(isSlave bool) {
func Init() {
if conf.RedisConfig.Server != "" && gin.Mode() != gin.TestMode {
Store = NewRedisStore(
10,
conf.RedisConfig.Network,
conf.RedisConfig.Server,
conf.RedisConfig.User,
conf.RedisConfig.Password,
conf.RedisConfig.DB,
)
}
}
if isSlave {
err := Store.Sets(conf.OptionOverwrite, "setting_")
if err != nil {
util.Log().Warning("Failed to overwrite database setting: %s", err)
}
// Restore restores cache from given disk file
func Restore(persistFile string) {
if err := Store.Restore(persistFile); err != nil {
util.Log().Warning("Failed to restore cache from disk: %s", err)
}
}
func InitSlaveOverwrites() {
err := Store.Sets(conf.OptionOverwrite, "setting_")
if err != nil {
util.Log().Warning("Failed to overwrite database setting: %s", err)
}
}
@@ -45,6 +58,12 @@ type Driver interface {
// 删除值
Delete(keys []string, prefix string) error
// Save in-memory cache to disk
Persist(path string) error
// Restore cache from disk
Restore(path string) error
}
// Set 设置缓存值
+6 -2
View File
@@ -56,10 +56,14 @@ func TestInit(t *testing.T) {
asserts := assert.New(t)
asserts.NotPanics(func() {
Init(false)
Init()
})
}
func TestInitSlaveOverwrites(t *testing.T) {
asserts := assert.New(t)
asserts.NotPanics(func() {
Init(true)
InitSlaveOverwrites()
})
}
+71 -8
View File
@@ -1,6 +1,9 @@
package cache
import (
"encoding/gob"
"fmt"
"os"
"sync"
"time"
@@ -14,18 +17,20 @@ type MemoStore struct {
// item 存储的对象
type itemWithTTL struct {
expires int64
value interface{}
Expires int64
Value interface{}
}
const DefaultCacheFile = "cache_persist.bin"
func newItem(value interface{}, expires int) itemWithTTL {
expires64 := int64(expires)
if expires > 0 {
expires64 = time.Now().Unix() + expires64
}
return itemWithTTL{
value: value,
expires: expires64,
Value: value,
Expires: expires64,
}
}
@@ -40,11 +45,11 @@ func getValue(item interface{}, ok bool) (interface{}, bool) {
return item, true
}
if itemObj.expires > 0 && itemObj.expires < time.Now().Unix() {
if itemObj.Expires > 0 && itemObj.Expires < time.Now().Unix() {
return nil, false
}
return itemObj.value, ok
return itemObj.Value, ok
}
@@ -52,7 +57,7 @@ func getValue(item interface{}, ok bool) (interface{}, bool) {
func (store *MemoStore) GarbageCollect() {
store.Store.Range(func(key, value interface{}) bool {
if item, ok := value.(itemWithTTL); ok {
if item.expires > 0 && item.expires < time.Now().Unix() {
if item.Expires > 0 && item.Expires < time.Now().Unix() {
util.Log().Debug("Cache %q is garbage collected.", key.(string))
store.Store.Delete(key)
}
@@ -98,7 +103,7 @@ func (store *MemoStore) Gets(keys []string, prefix string) (map[string]interface
// Sets 批量设置值
func (store *MemoStore) Sets(values map[string]interface{}, prefix string) error {
for key, value := range values {
store.Store.Store(prefix+key, value)
store.Store.Store(prefix+key, newItem(value, 0))
}
return nil
}
@@ -110,3 +115,61 @@ func (store *MemoStore) Delete(keys []string, prefix string) error {
}
return nil
}
// Persist write memory store into cache
func (store *MemoStore) Persist(path string) error {
persisted := make(map[string]itemWithTTL)
store.Store.Range(func(key, value interface{}) bool {
v, ok := store.Store.Load(key)
if _, ok := getValue(v, ok); ok {
persisted[key.(string)] = v.(itemWithTTL)
}
return true
})
res, err := serializer(persisted)
if err != nil {
return fmt.Errorf("failed to serialize cache: %s", err)
}
err = os.WriteFile(path, res, 0644)
return err
}
// Restore memory cache from disk file
func (store *MemoStore) Restore(path string) error {
if !util.Exists(path) {
return nil
}
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("failed to read cache file: %s", err)
}
defer func() {
f.Close()
os.Remove(path)
}()
persisted := &item{}
dec := gob.NewDecoder(f)
if err := dec.Decode(&persisted); err != nil {
return fmt.Errorf("unknown cache file format: %s", err)
}
items := persisted.Value.(map[string]itemWithTTL)
loaded := 0
for k, v := range items {
if _, ok := getValue(v, true); ok {
loaded++
store.Store.Store(k, v)
} else {
util.Log().Debug("Persisted cache %q is expired.", k)
}
}
util.Log().Info("Restored %d items from %q into memory cache.", loaded, path)
return nil
}
+45 -1
View File
@@ -2,6 +2,7 @@ package cache
import (
"github.com/stretchr/testify/assert"
"path/filepath"
"testing"
"time"
)
@@ -23,7 +24,7 @@ func TestMemoStore_Set(t *testing.T) {
val, ok := store.Store.Load("KEY")
asserts.True(ok)
asserts.Equal("vAL", val.(itemWithTTL).value)
asserts.Equal("vAL", val.(itemWithTTL).Value)
}
func TestMemoStore_Get(t *testing.T) {
@@ -145,3 +146,46 @@ func TestMemoStore_GarbageCollect(t *testing.T) {
_, ok := store.Get("test")
asserts.False(ok)
}
func TestMemoStore_PersistFailed(t *testing.T) {
a := assert.New(t)
store := NewMemoStore()
type testStruct struct{ v string }
store.Set("test", 1, 0)
store.Set("test2", testStruct{v: "test"}, 0)
err := store.Persist(filepath.Join(t.TempDir(), "TestMemoStore_PersistFailed"))
a.Error(err)
}
func TestMemoStore_PersistAndRestore(t *testing.T) {
a := assert.New(t)
store := NewMemoStore()
store.Set("test", 1, 0)
// already expired
store.Store.Store("test2", itemWithTTL{Value: "test", Expires: 1})
// expired after persist
store.Set("test3", 1, 1)
temp := filepath.Join(t.TempDir(), "TestMemoStore_PersistFailed")
// Persist
err := store.Persist(temp)
a.NoError(err)
a.FileExists(temp)
time.Sleep(2 * time.Second)
// Restore
store2 := NewMemoStore()
err = store2.Restore(temp)
a.NoError(err)
test, testOk := store2.Get("test")
a.EqualValues(1, test)
a.True(testOk)
test2, test2Ok := store2.Get("test2")
a.Nil(test2)
a.False(test2Ok)
test3, test3Ok := store2.Get("test3")
a.Nil(test3)
a.False(test3Ok)
a.NoFileExists(temp)
}
+13 -3
View File
@@ -44,7 +44,7 @@ func deserializer(value []byte) (interface{}, error) {
}
// NewRedisStore 创建新的redis存储
func NewRedisStore(size int, network, address, password, database string) *RedisStore {
func NewRedisStore(size int, network, address, user, password, database string) *RedisStore {
return &RedisStore{
pool: &redis.Pool{
MaxIdle: size,
@@ -63,11 +63,11 @@ func NewRedisStore(size int, network, address, password, database string) *Redis
network,
address,
redis.DialDatabase(db),
redis.DialUsername(user),
redis.DialPassword(password),
)
if err != nil {
util.Log().Warning("Failed to create Redis connection: %s", err)
return nil, err
util.Log().Panic("Failed to create Redis connection: %s", err)
}
return c, nil
},
@@ -215,3 +215,13 @@ func (store *RedisStore) DeleteAll() error {
return err
}
// Persist Dummy implementation
func (store *RedisStore) Persist(path string) error {
return nil
}
// Restore dummy implementation
func (store *RedisStore) Restore(path string) error {
return nil
}
+5 -5
View File
@@ -13,16 +13,16 @@ import (
func TestNewRedisStore(t *testing.T) {
asserts := assert.New(t)
store := NewRedisStore(10, "tcp", "", "", "0")
store := NewRedisStore(10, "tcp", "", "", "", "0")
asserts.NotNil(store)
conn, err := store.pool.Dial()
asserts.Nil(conn)
asserts.Error(err)
asserts.Panics(func() {
store.pool.Dial()
})
testConn := redigomock.NewConn()
cmd := testConn.Command("PING").Expect("PONG")
err = store.pool.TestOnBorrow(testConn, time.Now())
err := store.pool.TestOnBorrow(testConn, time.Now())
if testConn.Stats(cmd) != 1 {
fmt.Println("Command was not used")
return
+5 -5
View File
@@ -35,8 +35,8 @@ type Controller interface {
// Get master node info
GetMasterInfo(string) (*MasterInfo, error)
// Get master OneDrive policy credential
GetOneDriveToken(string, uint) (string, error)
// Get master Oauth based policy credential
GetPolicyOauthToken(string, uint) (string, error)
}
type slaveController struct {
@@ -181,8 +181,8 @@ func (c *slaveController) GetMasterInfo(id string) (*MasterInfo, error) {
return nil, ErrMasterNotFound
}
// GetOneDriveToken 获取主机OneDrive凭证
func (c *slaveController) GetOneDriveToken(id string, policyID uint) (string, error) {
// GetPolicyOauthToken 获取主机存储策略 Oauth 凭证
func (c *slaveController) GetPolicyOauthToken(id string, policyID uint) (string, error) {
c.lock.RLock()
if node, ok := c.masters[id]; ok {
@@ -190,7 +190,7 @@ func (c *slaveController) GetOneDriveToken(id string, policyID uint) (string, er
res, err := node.Client.Request(
"GET",
fmt.Sprintf("/api/v3/slave/credential/onedrive/%d", policyID),
fmt.Sprintf("/api/v3/slave/credential/%d", policyID),
nil,
).CheckHTTPResponse(200).DecodeResponse()
if err != nil {
+7 -7
View File
@@ -320,7 +320,7 @@ func TestSlaveController_GetOneDriveToken(t *testing.T) {
// node not exit
{
res, err := c.GetOneDriveToken("2", 1)
res, err := c.GetPolicyOauthToken("2", 1)
a.Equal(ErrMasterNotFound, err)
a.Empty(res)
}
@@ -328,7 +328,7 @@ func TestSlaveController_GetOneDriveToken(t *testing.T) {
// return none 200
{
mockRequest := &requestMock{}
mockRequest.On("Request", "GET", "/api/v3/slave/credential/onedrive/1", testMock.Anything, testMock.Anything).Return(&request.Response{
mockRequest.On("Request", "GET", "/api/v3/slave/credential/1", testMock.Anything, testMock.Anything).Return(&request.Response{
Response: &http.Response{StatusCode: http.StatusConflict},
})
c := &slaveController{
@@ -336,7 +336,7 @@ func TestSlaveController_GetOneDriveToken(t *testing.T) {
"1": {Client: mockRequest},
},
}
res, err := c.GetOneDriveToken("1", 1)
res, err := c.GetPolicyOauthToken("1", 1)
a.Error(err)
a.Empty(res)
mockRequest.AssertExpectations(t)
@@ -345,7 +345,7 @@ func TestSlaveController_GetOneDriveToken(t *testing.T) {
// master return error
{
mockRequest := &requestMock{}
mockRequest.On("Request", "GET", "/api/v3/slave/credential/onedrive/1", testMock.Anything, testMock.Anything).Return(&request.Response{
mockRequest.On("Request", "GET", "/api/v3/slave/credential/1", testMock.Anything, testMock.Anything).Return(&request.Response{
Response: &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(strings.NewReader("{\"code\":1}")),
@@ -356,7 +356,7 @@ func TestSlaveController_GetOneDriveToken(t *testing.T) {
"1": {Client: mockRequest},
},
}
res, err := c.GetOneDriveToken("1", 1)
res, err := c.GetPolicyOauthToken("1", 1)
a.Equal(1, err.(serializer.AppError).Code)
a.Empty(res)
mockRequest.AssertExpectations(t)
@@ -365,7 +365,7 @@ func TestSlaveController_GetOneDriveToken(t *testing.T) {
// success
{
mockRequest := &requestMock{}
mockRequest.On("Request", "GET", "/api/v3/slave/credential/onedrive/1", testMock.Anything, testMock.Anything).Return(&request.Response{
mockRequest.On("Request", "GET", "/api/v3/slave/credential/1", testMock.Anything, testMock.Anything).Return(&request.Response{
Response: &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(strings.NewReader("{\"data\":\"expected\"}")),
@@ -376,7 +376,7 @@ func TestSlaveController_GetOneDriveToken(t *testing.T) {
"1": {Client: mockRequest},
},
}
res, err := c.GetOneDriveToken("1", 1)
res, err := c.GetPolicyOauthToken("1", 1)
a.NoError(err)
a.Equal("expected", res)
mockRequest.AssertExpectations(t)
+2
View File
@@ -17,6 +17,7 @@ type database struct {
DBFile string
Port int
Charset string
UnixSocket bool
}
// system 系统通用配置
@@ -52,6 +53,7 @@ type slave struct {
type redis struct {
Network string
Server string
User string
Password string
DB string
}
+5 -4
View File
@@ -10,10 +10,11 @@ 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 系统公用配置
+3 -3
View File
@@ -1,13 +1,13 @@
package conf
// BackendVersion 当前后端版本号
var BackendVersion = "3.6.2"
var BackendVersion = "3.8.3"
// RequiredDBVersion 与当前版本匹配的数据库版本
var RequiredDBVersion = "3.6.0"
var RequiredDBVersion = "3.8.1"
// RequiredStaticVersion 与当前版本匹配的静态资源版本
var RequiredStaticVersion = "3.6.2"
var RequiredStaticVersion = "3.8.3"
// IsPro 是否为Pro版本
var IsPro = "false"
+1 -1
View File
@@ -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)
}
+22 -20
View File
@@ -17,10 +17,12 @@ import (
"time"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
"github.com/cloudreve/Cloudreve/v3/pkg/request"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/google/go-querystring/query"
cossdk "github.com/tencentyun/cos-go-sdk-v5"
)
@@ -148,14 +150,7 @@ func (handler Driver) CORS() error {
// Get 获取文件
func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser, error) {
// 获取文件源地址
downloadURL, err := handler.Source(
ctx,
path,
url.URL{},
int64(model.GetIntSetting("preview_timeout", 60)),
false,
0,
)
downloadURL, err := handler.Source(ctx, path, int64(model.GetIntSetting("preview_timeout", 60)), false, 0)
if err != nil {
return nil, err
}
@@ -222,19 +217,33 @@ func (handler Driver) Delete(ctx context.Context, files []string) ([]string, err
}
// Thumb 获取文件缩略图
func (handler Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) {
func (handler Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) {
// quick check by extension name
// https://cloud.tencent.com/document/product/436/44893
supported := []string{"png", "jpg", "jpeg", "gif", "bmp", "webp", "heif", "heic"}
if len(handler.Policy.OptionsSerialized.ThumbExts) > 0 {
supported = handler.Policy.OptionsSerialized.ThumbExts
}
if !util.IsInExtensionList(supported, file.Name) || file.Size > (32<<(10*2)) {
return nil, driver.ErrorThumbNotSupported
}
var (
thumbSize = [2]uint{400, 300}
ok = false
)
if thumbSize, ok = ctx.Value(fsctx.ThumbSizeCtx).([2]uint); !ok {
return nil, errors.New("无法获取缩略图尺寸设置")
return nil, errors.New("failed to get thumbnail size")
}
thumbParam := fmt.Sprintf("imageMogr2/thumbnail/%dx%d", thumbSize[0], thumbSize[1])
thumbEncodeQuality := model.GetIntSetting("thumb_encode_quality", 85)
thumbParam := fmt.Sprintf("imageMogr2/thumbnail/%dx%d/quality/%d", thumbSize[0], thumbSize[1], thumbEncodeQuality)
source, err := handler.signSourceURL(
ctx,
path,
file.SourceName,
int64(model.GetIntSetting("preview_timeout", 60)),
&urlOption{},
)
@@ -254,14 +263,7 @@ func (handler Driver) Thumb(ctx context.Context, path string) (*response.Content
}
// Source 获取外链URL
func (handler Driver) Source(
ctx context.Context,
path string,
baseURL url.URL,
ttl int64,
isDownload bool,
speed int,
) (string, error) {
func (handler Driver) Source(ctx context.Context, path string, ttl int64, isDownload bool, speed int) (string, error) {
// 尝试从上下文获取文件名
fileName := ""
if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok {
@@ -0,0 +1,73 @@
package googledrive
import (
"errors"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
"github.com/cloudreve/Cloudreve/v3/pkg/request"
"google.golang.org/api/drive/v3"
)
// Client Google Drive client
type Client struct {
Endpoints *Endpoints
Policy *model.Policy
Credential *Credential
ClientID string
ClientSecret string
Redirect string
Request request.Client
ClusterController cluster.Controller
}
// Endpoints OneDrive客户端相关设置
type Endpoints struct {
UserConsentEndpoint string // OAuth认证的基URL
TokenEndpoint string // OAuth token 基URL
EndpointURL string // 接口请求的基URL
}
const (
TokenCachePrefix = "googledrive_"
oauthEndpoint = "https://oauth2.googleapis.com/token"
userConsentBase = "https://accounts.google.com/o/oauth2/auth"
v3DriveEndpoint = "https://www.googleapis.com/drive/v3"
)
var (
// Defualt required scopes
RequiredScope = []string{
drive.DriveScope,
"openid",
"profile",
"https://www.googleapis.com/auth/userinfo.profile",
}
// ErrInvalidRefreshToken 上传策略无有效的RefreshToken
ErrInvalidRefreshToken = errors.New("no valid refresh token in this policy")
)
// NewClient 根据存储策略获取新的client
func NewClient(policy *model.Policy) (*Client, error) {
client := &Client{
Endpoints: &Endpoints{
TokenEndpoint: oauthEndpoint,
UserConsentEndpoint: userConsentBase,
EndpointURL: v3DriveEndpoint,
},
Credential: &Credential{
RefreshToken: policy.AccessKey,
},
Policy: policy,
ClientID: policy.BucketName,
ClientSecret: policy.SecretKey,
Redirect: policy.OptionsSerialized.OauthRedirect,
Request: request.NewClient(),
ClusterController: cluster.DefaultController,
}
return client, nil
}
@@ -0,0 +1,65 @@
package googledrive
import (
"context"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
"github.com/cloudreve/Cloudreve/v3/pkg/request"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
)
// Driver Google Drive 适配器
type Driver struct {
Policy *model.Policy
HTTPClient request.Client
}
// NewDriver 从存储策略初始化新的Driver实例
func NewDriver(policy *model.Policy) (driver.Handler, error) {
return &Driver{
Policy: policy,
HTTPClient: request.NewClient(),
}, nil
}
func (d *Driver) Put(ctx context.Context, file fsctx.FileHeader) error {
//TODO implement me
panic("implement me")
}
func (d *Driver) Delete(ctx context.Context, files []string) ([]string, error) {
//TODO implement me
panic("implement me")
}
func (d *Driver) Get(ctx context.Context, path string) (response.RSCloser, error) {
//TODO implement me
panic("implement me")
}
func (d *Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) {
//TODO implement me
panic("implement me")
}
func (d *Driver) Source(ctx context.Context, path string, ttl int64, isDownload bool, speed int) (string, error) {
//TODO implement me
panic("implement me")
}
func (d *Driver) Token(ctx context.Context, ttl int64, uploadSession *serializer.UploadSession, file fsctx.FileHeader) (*serializer.UploadCredential, error) {
//TODO implement me
panic("implement me")
}
func (d *Driver) CancelToken(ctx context.Context, uploadSession *serializer.UploadSession) error {
//TODO implement me
panic("implement me")
}
func (d *Driver) List(ctx context.Context, path string, recursive bool) ([]response.Object, error) {
//TODO implement me
panic("implement me")
}
+154
View File
@@ -0,0 +1,154 @@
package googledrive
import (
"context"
"encoding/json"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/oauth"
"github.com/cloudreve/Cloudreve/v3/pkg/request"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// OAuthURL 获取OAuth认证页面URL
func (client *Client) OAuthURL(ctx context.Context, scope []string) string {
query := url.Values{
"client_id": {client.ClientID},
"scope": {strings.Join(scope, " ")},
"response_type": {"code"},
"redirect_uri": {client.Redirect},
"access_type": {"offline"},
"prompt": {"consent"},
}
u, _ := url.Parse(client.Endpoints.UserConsentEndpoint)
u.RawQuery = query.Encode()
return u.String()
}
// ObtainToken 通过code或refresh_token兑换token
func (client *Client) ObtainToken(ctx context.Context, code, refreshToken string) (*Credential, error) {
body := url.Values{
"client_id": {client.ClientID},
"redirect_uri": {client.Redirect},
"client_secret": {client.ClientSecret},
}
if code != "" {
body.Add("grant_type", "authorization_code")
body.Add("code", code)
} else {
body.Add("grant_type", "refresh_token")
body.Add("refresh_token", refreshToken)
}
strBody := body.Encode()
res := client.Request.Request(
"POST",
client.Endpoints.TokenEndpoint,
io.NopCloser(strings.NewReader(strBody)),
request.WithHeader(http.Header{
"Content-Type": {"application/x-www-form-urlencoded"}},
),
request.WithContentLength(int64(len(strBody))),
)
if res.Err != nil {
return nil, res.Err
}
respBody, err := res.GetResponse()
if err != nil {
return nil, err
}
var (
errResp OAuthError
credential Credential
decodeErr error
)
if res.Response.StatusCode != 200 {
decodeErr = json.Unmarshal([]byte(respBody), &errResp)
} else {
decodeErr = json.Unmarshal([]byte(respBody), &credential)
}
if decodeErr != nil {
return nil, decodeErr
}
if errResp.ErrorType != "" {
return nil, errResp
}
return &credential, nil
}
// UpdateCredential 更新凭证,并检查有效期
func (client *Client) UpdateCredential(ctx context.Context, isSlave bool) error {
if isSlave {
return client.fetchCredentialFromMaster(ctx)
}
oauth.GlobalMutex.Lock(client.Policy.ID)
defer oauth.GlobalMutex.Unlock(client.Policy.ID)
// 如果已存在凭证
if client.Credential != nil && client.Credential.AccessToken != "" {
// 检查已有凭证是否过期
if client.Credential.ExpiresIn > time.Now().Unix() {
// 未过期,不要更新
return nil
}
}
// 尝试从缓存中获取凭证
if cacheCredential, ok := cache.Get(TokenCachePrefix + client.ClientID); ok {
credential := cacheCredential.(Credential)
if credential.ExpiresIn > time.Now().Unix() {
client.Credential = &credential
return nil
}
}
// 获取新的凭证
if client.Credential == nil || client.Credential.RefreshToken == "" {
// 无有效的RefreshToken
util.Log().Error("Failed to refresh credential for policy %q, please login your Google account again.", client.Policy.Name)
return ErrInvalidRefreshToken
}
credential, err := client.ObtainToken(ctx, "", client.Credential.RefreshToken)
if err != nil {
return err
}
// 更新有效期为绝对时间戳
expires := credential.ExpiresIn - 60
credential.ExpiresIn = time.Now().Add(time.Duration(expires) * time.Second).Unix()
// refresh token for Google Drive does not expire in production
credential.RefreshToken = client.Credential.RefreshToken
client.Credential = credential
// 更新缓存
cache.Set(TokenCachePrefix+client.ClientID, *credential, int(expires))
return nil
}
func (client *Client) AccessToken() string {
return client.Credential.AccessToken
}
// UpdateCredential 更新凭证,并检查有效期
func (client *Client) fetchCredentialFromMaster(ctx context.Context) error {
res, err := client.ClusterController.GetPolicyOauthToken(client.Policy.MasterID, client.Policy.ID)
if err != nil {
return err
}
client.Credential = &Credential{AccessToken: res}
return nil
}
@@ -0,0 +1,43 @@
package googledrive
import "encoding/gob"
// RespError 接口返回错误
type RespError struct {
APIError APIError `json:"error"`
}
// APIError 接口返回的错误内容
type APIError struct {
Code string `json:"code"`
Message string `json:"message"`
}
// Error 实现error接口
func (err RespError) Error() string {
return err.APIError.Message
}
// Credential 获取token时返回的凭证
type Credential struct {
ExpiresIn int64 `json:"expires_in"`
Scope string `json:"scope"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
UserID string `json:"user_id"`
}
// OAuthError OAuth相关接口的错误响应
type OAuthError struct {
ErrorType string `json:"error"`
ErrorDescription string `json:"error_description"`
}
// Error 实现error接口
func (err OAuthError) Error() string {
return err.ErrorDescription
}
func init() {
gob.Register(Credential{})
}
+12 -3
View File
@@ -2,10 +2,16 @@ package driver
import (
"context"
"fmt"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"net/url"
)
var (
ErrorThumbNotExist = fmt.Errorf("thumb not exist")
ErrorThumbNotSupported = fmt.Errorf("thumb not supported")
)
// Handler 存储策略适配器
@@ -22,12 +28,15 @@ type Handler interface {
// 获取缩略图,可直接在ContentResponse中返回文件数据流,也可指
// 定为重定向
Thumb(ctx context.Context, path string) (*response.ContentResponse, error)
// 如果缩略图不存在, 且需要 Cloudreve 代理生成并上传,应返回 ErrorThumbNotExist,生
// 成的缩略图文件存储规则与本机策略一致。
// 如果不支持此文件的缩略图,并且不希望后续继续请求此缩略图,应返回 ErrorThumbNotSupported
Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error)
// 获取外链/下载地址,
// url - 站点本身地址,
// isDownload - 是否直接下载
Source(ctx context.Context, path string, url url.URL, ttl int64, isDownload bool, speed int) (string, error)
Source(ctx context.Context, path string, ttl int64, isDownload bool, speed int) (string, error)
// Token 获取有效期为ttl的上传凭证和签名
Token(ctx context.Context, ttl int64, uploadSession *serializer.UploadSession, file fsctx.FileHeader) (*serializer.UploadCredential, error)
+23 -13
View File
@@ -12,6 +12,8 @@ import (
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/conf"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
@@ -194,39 +196,43 @@ func (handler Driver) Delete(ctx context.Context, files []string) ([]string, err
}
// Thumb 获取文件缩略图
func (handler Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) {
file, err := handler.Get(ctx, path+model.GetSettingByNameWithDefault("thumb_file_suffix", "._thumb"))
func (handler Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) {
// Quick check thumb existence on master.
if conf.SystemConfig.Mode == "master" && file.MetadataSerialized[model.ThumbStatusMetadataKey] == model.ThumbStatusNotExist {
// Tell invoker to generate a thumb
return nil, driver.ErrorThumbNotExist
}
thumbFile, err := handler.Get(ctx, file.ThumbFile())
if err != nil {
if errors.Is(err, os.ErrNotExist) {
err = fmt.Errorf("thumb not exist: %w (%w)", err, driver.ErrorThumbNotExist)
}
return nil, err
}
return &response.ContentResponse{
Redirect: false,
Content: file,
Content: thumbFile,
}, nil
}
// Source 获取外链URL
func (handler Driver) Source(
ctx context.Context,
path string,
baseURL url.URL,
ttl int64,
isDownload bool,
speed int,
) (string, error) {
func (handler Driver) Source(ctx context.Context, path string, ttl int64, isDownload bool, speed int) (string, error) {
file, ok := ctx.Value(fsctx.FileModelCtx).(model.File)
if !ok {
return "", errors.New("failed to read file model context")
}
var baseURL *url.URL
// 是否启用了CDN
if handler.Policy.BaseURL != "" {
cdnURL, err := url.Parse(handler.Policy.BaseURL)
if err != nil {
return "", err
}
baseURL = *cdnURL
baseURL = cdnURL
}
var (
@@ -260,7 +266,11 @@ func (handler Driver) Source(
return "", serializer.NewError(serializer.CodeEncryptError, "Failed to sign url", err)
}
finalURL := baseURL.ResolveReference(signedURI).String()
finalURL := signedURI.String()
if baseURL != nil {
finalURL = baseURL.ResolveReference(signedURI).String()
}
return finalURL, nil
}
+28 -28
View File
@@ -4,13 +4,13 @@ import (
"context"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/jinzhu/gorm"
"github.com/stretchr/testify/assert"
"io"
"net/url"
"os"
"strings"
"testing"
@@ -141,17 +141,34 @@ func TestHandler_Thumb(t *testing.T) {
asserts.NoError(err)
file.Close()
f := &model.File{
SourceName: "TestHandler_Thumb",
MetadataSerialized: map[string]string{
model.ThumbStatusMetadataKey: model.ThumbStatusExist,
},
}
// 正常
{
thumb, err := handler.Thumb(ctx, "TestHandler_Thumb")
thumb, err := handler.Thumb(ctx, f)
asserts.NoError(err)
asserts.NotNil(thumb.Content)
}
// 不存在
// file 不存在
{
_, err := handler.Thumb(ctx, "not_exist")
f.SourceName = "not_exist"
_, err := handler.Thumb(ctx, f)
asserts.Error(err)
asserts.ErrorIs(err, driver.ErrorThumbNotExist)
}
// thumb not exist
{
f.MetadataSerialized[model.ThumbStatusMetadataKey] = model.ThumbStatusNotExist
_, err := handler.Thumb(ctx, f)
asserts.Error(err)
asserts.ErrorIs(err, driver.ErrorThumbNotExist)
}
}
@@ -172,13 +189,10 @@ func TestHandler_Source(t *testing.T) {
Name: "test.jpg",
}
ctx := context.WithValue(ctx, fsctx.FileModelCtx, file)
baseURL, err := url.Parse("https://cloudreve.org")
asserts.NoError(err)
sourceURL, err := handler.Source(ctx, "", *baseURL, 0, false, 0)
sourceURL, err := handler.Source(ctx, "", 0, false, 0)
asserts.NoError(err)
asserts.NotEmpty(sourceURL)
asserts.Contains(sourceURL, "sign=")
asserts.Contains(sourceURL, "https://cloudreve.org")
}
// 下载
@@ -190,21 +204,16 @@ func TestHandler_Source(t *testing.T) {
Name: "test.jpg",
}
ctx := context.WithValue(ctx, fsctx.FileModelCtx, file)
baseURL, err := url.Parse("https://cloudreve.org")
asserts.NoError(err)
sourceURL, err := handler.Source(ctx, "", *baseURL, 0, true, 0)
sourceURL, err := handler.Source(ctx, "", 0, true, 0)
asserts.NoError(err)
asserts.NotEmpty(sourceURL)
asserts.Contains(sourceURL, "sign=")
asserts.Contains(sourceURL, "download")
asserts.Contains(sourceURL, "https://cloudreve.org")
}
// 无法获取上下文
{
baseURL, err := url.Parse("https://cloudreve.org")
asserts.NoError(err)
sourceURL, err := handler.Source(ctx, "", *baseURL, 0, false, 0)
sourceURL, err := handler.Source(ctx, "", 0, false, 0)
asserts.Error(err)
asserts.Empty(sourceURL)
}
@@ -219,9 +228,7 @@ func TestHandler_Source(t *testing.T) {
Name: "test.jpg",
}
ctx := context.WithValue(ctx, fsctx.FileModelCtx, file)
baseURL, err := url.Parse("https://cloudreve.org")
asserts.NoError(err)
sourceURL, err := handler.Source(ctx, "", *baseURL, 0, false, 0)
sourceURL, err := handler.Source(ctx, "", 0, false, 0)
asserts.NoError(err)
asserts.NotEmpty(sourceURL)
asserts.Contains(sourceURL, "sign=")
@@ -238,9 +245,7 @@ func TestHandler_Source(t *testing.T) {
Name: "test.jpg",
}
ctx := context.WithValue(ctx, fsctx.FileModelCtx, file)
baseURL, err := url.Parse("https://cloudreve.org")
asserts.NoError(err)
sourceURL, err := handler.Source(ctx, "", *baseURL, 0, false, 0)
sourceURL, err := handler.Source(ctx, "", 0, false, 0)
asserts.Error(err)
asserts.Empty(sourceURL)
}
@@ -261,19 +266,14 @@ func TestHandler_GetDownloadURL(t *testing.T) {
Name: "test.jpg",
}
ctx := context.WithValue(ctx, fsctx.FileModelCtx, file)
baseURL, err := url.Parse("https://cloudreve.org")
asserts.NoError(err)
downloadURL, err := handler.Source(ctx, "", *baseURL, 10, true, 0)
downloadURL, err := handler.Source(ctx, "", 10, true, 0)
asserts.NoError(err)
asserts.Contains(downloadURL, "sign=")
asserts.Contains(downloadURL, "https://cloudreve.org")
}
// 无法获取上下文
{
baseURL, err := url.Parse("https://cloudreve.org")
asserts.NoError(err)
downloadURL, err := handler.Source(ctx, "", *baseURL, 10, true, 0)
downloadURL, err := handler.Source(ctx, "", 10, true, 0)
asserts.Error(err)
asserts.Empty(downloadURL)
}
+4 -3
View File
@@ -3,7 +3,6 @@ package onedrive
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
"io"
@@ -32,6 +31,8 @@ const (
// ListRetry 列取请求重试次数
ListRetry = 1
chunkRetrySleep = time.Second * 5
notFoundError = "itemNotFound"
)
// GetSourcePath 获取文件的绝对路径
@@ -438,7 +439,7 @@ func (client *Client) GetThumbURL(ctx context.Context, dst string, w, h uint) (s
}
}
return "", errors.New("failed to generate thumb")
return "", ErrThumbSizeNotFound
}
// MonitorUpload 监控客户端分片上传进度
@@ -469,7 +470,7 @@ func (client *Client) MonitorUpload(uploadURL, callbackKey, path string, size ui
if err != nil {
if resErr, ok := err.(*RespError); ok {
if resErr.APIError.Code == "itemNotFound" {
if resErr.APIError.Code == notFoundError {
util.Log().Debug("Upload completed, will check upload callback later.")
select {
case <-time.After(time.Duration(interval) * time.Second):
+3 -1
View File
@@ -17,6 +17,8 @@ var (
ErrDeleteFile = errors.New("cannot delete file")
// ErrClientCanceled 客户端取消操作
ErrClientCanceled = errors.New("client canceled")
// Desired thumb size not available
ErrThumbSizeNotFound = errors.New("thumb size not found")
)
// Client OneDrive客户端
@@ -56,7 +58,7 @@ func NewClient(policy *model.Policy) (*Client, error) {
Policy: policy,
ClientID: policy.BucketName,
ClientSecret: policy.SecretKey,
Redirect: policy.OptionsSerialized.OdRedirect,
Redirect: policy.OptionsSerialized.OauthRedirect,
Request: request.NewClient(),
ClusterController: cluster.DefaultController,
}
+7 -7
View File
@@ -91,7 +91,6 @@ func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser,
downloadURL, err := handler.Source(
ctx,
path,
url.URL{},
60,
false,
0,
@@ -136,7 +135,7 @@ func (handler Driver) Delete(ctx context.Context, files []string) ([]string, err
}
// Thumb 获取文件缩略图
func (handler Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) {
func (handler Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) {
var (
thumbSize = [2]uint{400, 300}
ok = false
@@ -145,13 +144,15 @@ func (handler Driver) Thumb(ctx context.Context, path string) (*response.Content
return nil, errors.New("failed to get thumbnail size")
}
res, err := handler.Client.GetThumbURL(ctx, path, thumbSize[0], thumbSize[1])
res, err := handler.Client.GetThumbURL(ctx, file.SourceName, thumbSize[0], thumbSize[1])
if err != nil {
// 如果出现异常,就清空文件的pic_info
if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok {
file.UpdatePicInfo("")
var apiErr *RespError
if errors.As(err, &apiErr); err == ErrThumbSizeNotFound || (apiErr != nil && apiErr.APIError.Code == notFoundError) {
// OneDrive cannot generate thumbnail for this file
return nil, driver.ErrorThumbNotSupported
}
}
return &response.ContentResponse{
Redirect: true,
URL: res,
@@ -162,7 +163,6 @@ func (handler Driver) Thumb(ctx context.Context, path string) (*response.Content
func (handler Driver) Source(
ctx context.Context,
path string,
baseURL url.URL,
ttl int64,
isDownload bool,
speed int,
@@ -9,7 +9,6 @@ import (
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"testing"
"time"
@@ -106,7 +105,7 @@ func TestDriver_Source(t *testing.T) {
// 失败
{
res, err := handler.Source(context.Background(), "123.jpg", url.URL{}, 1, true, 0)
res, err := handler.Source(context.Background(), "123.jpg", 1, true, 0)
asserts.Error(err)
asserts.Empty(res)
}
@@ -116,7 +115,7 @@ func TestDriver_Source(t *testing.T) {
handler.Client.Credential.ExpiresIn = time.Now().Add(time.Duration(100) * time.Hour).Unix()
handler.Client.Credential.AccessToken = "1"
cache.Set("onedrive_source_0_123.jpg", "res", 1)
res, err := handler.Source(context.Background(), "123.jpg", url.URL{}, 0, true, 0)
res, err := handler.Source(context.Background(), "123.jpg", 0, true, 0)
cache.Deletes([]string{"0_123.jpg"}, "onedrive_source_")
asserts.NoError(err)
asserts.Equal("res", res)
@@ -131,7 +130,7 @@ func TestDriver_Source(t *testing.T) {
handler.Client.Credential.ExpiresIn = time.Now().Add(time.Duration(100) * time.Hour).Unix()
handler.Client.Credential.AccessToken = "1"
cache.Set(fmt.Sprintf("onedrive_source_file_%d_1", file.UpdatedAt.Unix()), "res", 0)
res, err := handler.Source(ctx, "123.jpg", url.URL{}, 1, true, 0)
res, err := handler.Source(ctx, "123.jpg", 1, true, 0)
cache.Deletes([]string{"0_123.jpg"}, "onedrive_source_")
asserts.NoError(err)
asserts.Equal("res", res)
@@ -156,7 +155,7 @@ func TestDriver_Source(t *testing.T) {
})
handler.Client.Request = clientMock
handler.Client.Credential.AccessToken = "1"
res, err := handler.Source(context.Background(), "123.jpg", url.URL{}, 1, true, 0)
res, err := handler.Source(context.Background(), "123.jpg", 1, true, 0)
asserts.NoError(err)
asserts.Equal("123321", res)
}
@@ -246,19 +245,19 @@ func TestDriver_Thumb(t *testing.T) {
}
handler.Client, _ = NewClient(&model.Policy{})
handler.Client.Credential.ExpiresIn = time.Now().Add(time.Duration(100) * time.Hour).Unix()
file := &model.File{PicInfo: "1,1", Model: gorm.Model{ID: 1}}
// 失败
{
ctx := context.WithValue(context.Background(), fsctx.ThumbSizeCtx, [2]uint{10, 20})
ctx = context.WithValue(ctx, fsctx.FileModelCtx, model.File{PicInfo: "1,1", Model: gorm.Model{ID: 1}})
res, err := handler.Thumb(ctx, "123.jpg")
res, err := handler.Thumb(ctx, file)
asserts.Error(err)
asserts.Empty(res.URL)
}
// 上下文错误
{
_, err := handler.Thumb(context.Background(), "123.jpg")
_, err := handler.Thumb(context.Background(), file)
asserts.Error(err)
}
}
+8 -3
View File
@@ -10,6 +10,7 @@ import (
"time"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/oauth"
"github.com/cloudreve/Cloudreve/v3/pkg/request"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
)
@@ -128,8 +129,8 @@ func (client *Client) UpdateCredential(ctx context.Context, isSlave bool) error
return client.fetchCredentialFromMaster(ctx)
}
GlobalMutex.Lock(client.Policy.ID)
defer GlobalMutex.Unlock(client.Policy.ID)
oauth.GlobalMutex.Lock(client.Policy.ID)
defer oauth.GlobalMutex.Unlock(client.Policy.ID)
// 如果已存在凭证
if client.Credential != nil && client.Credential.AccessToken != "" {
@@ -175,9 +176,13 @@ func (client *Client) UpdateCredential(ctx context.Context, isSlave bool) error
return nil
}
func (client *Client) AccessToken() string {
return client.Credential.AccessToken
}
// UpdateCredential 更新凭证,并检查有效期
func (client *Client) fetchCredentialFromMaster(ctx context.Context) error {
res, err := client.ClusterController.GetOneDriveToken(client.Policy.MasterID, client.Policy.ID)
res, err := client.ClusterController.GetPolicyOauthToken(client.Policy.MasterID, client.Policy.ID)
if err != nil {
return err
}
+2 -2
View File
@@ -368,7 +368,7 @@ func TestClient_UpdateCredential(t *testing.T) {
// slave failed
{
mockController := &controllermock.SlaveControllerMock{}
mockController.On("GetOneDriveToken", testMock.Anything, testMock.Anything).Return("", errors.New("error"))
mockController.On("GetPolicyOauthToken", testMock.Anything, testMock.Anything).Return("", errors.New("error"))
client.ClusterController = mockController
err := client.UpdateCredential(context.Background(), true)
asserts.Error(err)
@@ -377,7 +377,7 @@ func TestClient_UpdateCredential(t *testing.T) {
// slave success
{
mockController := &controllermock.SlaveControllerMock{}
mockController.On("GetOneDriveToken", testMock.Anything, testMock.Anything).Return("AccessToken3", nil)
mockController.On("GetPolicyOauthToken", testMock.Anything, testMock.Anything).Return("AccessToken3", nil)
client.ClusterController = mockController
err := client.UpdateCredential(context.Background(), true)
asserts.NoError(err)
+20 -19
View File
@@ -17,6 +17,7 @@ import (
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/chunk"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/chunk/backoff"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
"github.com/cloudreve/Cloudreve/v3/pkg/request"
@@ -193,14 +194,7 @@ func (handler *Driver) Get(ctx context.Context, path string) (response.RSCloser,
ctx = context.WithValue(ctx, fsctx.ForceUsePublicEndpointCtx, false)
// 获取文件源地址
downloadURL, err := handler.Source(
ctx,
path,
url.URL{},
int64(model.GetIntSetting("preview_timeout", 60)),
false,
0,
)
downloadURL, err := handler.Source(ctx, path, int64(model.GetIntSetting("preview_timeout", 60)), false, 0)
if err != nil {
return nil, err
}
@@ -293,7 +287,18 @@ func (handler *Driver) Delete(ctx context.Context, files []string) ([]string, er
}
// Thumb 获取文件缩略图
func (handler *Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) {
func (handler *Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) {
// quick check by extension name
// https://help.aliyun.com/document_detail/183902.html
supported := []string{"png", "jpg", "jpeg", "gif", "bmp", "webp", "heic", "tiff", "avif"}
if len(handler.Policy.OptionsSerialized.ThumbExts) > 0 {
supported = handler.Policy.OptionsSerialized.ThumbExts
}
if !util.IsInExtensionList(supported, file.Name) || file.Size > (20<<(10*2)) {
return nil, driver.ErrorThumbNotSupported
}
// 初始化客户端
if err := handler.InitOSSClient(true); err != nil {
return nil, err
@@ -307,12 +312,14 @@ func (handler *Driver) Thumb(ctx context.Context, path string) (*response.Conten
return nil, errors.New("failed to get thumbnail size")
}
thumbParam := fmt.Sprintf("image/resize,m_lfit,h_%d,w_%d", thumbSize[1], thumbSize[0])
thumbEncodeQuality := model.GetIntSetting("thumb_encode_quality", 85)
thumbParam := fmt.Sprintf("image/resize,m_lfit,h_%d,w_%d/quality,q_%d", thumbSize[1], thumbSize[0], thumbEncodeQuality)
ctx = context.WithValue(ctx, fsctx.ThumbSizeCtx, thumbParam)
thumbOption := []oss.Option{oss.Process(thumbParam)}
thumbURL, err := handler.signSourceURL(
ctx,
path,
file.SourceName,
int64(model.GetIntSetting("preview_timeout", 60)),
thumbOption,
)
@@ -327,14 +334,7 @@ func (handler *Driver) Thumb(ctx context.Context, path string) (*response.Conten
}
// Source 获取外链URL
func (handler *Driver) Source(
ctx context.Context,
path string,
baseURL url.URL,
ttl int64,
isDownload bool,
speed int,
) (string, error) {
func (handler *Driver) Source(ctx context.Context, path string, ttl int64, isDownload bool, speed int) (string, error) {
// 初始化客户端
usePublicEndpoint := true
if forceUsePublicEndpoint, ok := ctx.Value(fsctx.ForceUsePublicEndpointCtx).(bool); ok {
@@ -435,6 +435,7 @@ func (handler *Driver) Token(ctx context.Context, ttl int64, uploadSession *seri
options := []oss.Option{
oss.Expires(time.Now().Add(time.Duration(ttl) * time.Second)),
oss.ForbidOverWrite(true),
oss.ContentType(fileInfo.DetectMimeType()),
}
imur, err := handler.bucket.InitiateMultipartUpload(fileInfo.SavePath, options...)
if err != nil {
+21 -20
View File
@@ -13,10 +13,12 @@ import (
"time"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
"github.com/cloudreve/Cloudreve/v3/pkg/request"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/qiniu/go-sdk/v7/auth/qbox"
"github.com/qiniu/go-sdk/v7/storage"
)
@@ -117,14 +119,7 @@ func (handler *Driver) Get(ctx context.Context, path string) (response.RSCloser,
path = fmt.Sprintf("%s?v=%d", path, time.Now().UnixNano())
// 获取文件源地址
downloadURL, err := handler.Source(
ctx,
path,
url.URL{},
int64(model.GetIntSetting("preview_timeout", 60)),
false,
0,
)
downloadURL, err := handler.Source(ctx, path, int64(model.GetIntSetting("preview_timeout", 60)), false, 0)
if err != nil {
return nil, err
}
@@ -230,35 +225,41 @@ func (handler *Driver) Delete(ctx context.Context, files []string) ([]string, er
}
// Thumb 获取文件缩略图
func (handler *Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) {
func (handler *Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) {
// quick check by extension name
// https://developer.qiniu.com/dora/api/basic-processing-images-imageview2
supported := []string{"png", "jpg", "jpeg", "gif", "bmp", "webp", "tiff", "avif", "psd"}
if len(handler.Policy.OptionsSerialized.ThumbExts) > 0 {
supported = handler.Policy.OptionsSerialized.ThumbExts
}
if !util.IsInExtensionList(supported, file.Name) || file.Size > (20<<(10*2)) {
return nil, driver.ErrorThumbNotSupported
}
var (
thumbSize = [2]uint{400, 300}
ok = false
)
if thumbSize, ok = ctx.Value(fsctx.ThumbSizeCtx).([2]uint); !ok {
return nil, errors.New("无法获取缩略图尺寸设置")
return nil, errors.New("failed to get thumbnail size")
}
path = fmt.Sprintf("%s?imageView2/1/w/%d/h/%d", path, thumbSize[0], thumbSize[1])
thumbEncodeQuality := model.GetIntSetting("thumb_encode_quality", 85)
thumb := fmt.Sprintf("%s?imageView2/1/w/%d/h/%d/q/%d", file.SourceName, thumbSize[0], thumbSize[1], thumbEncodeQuality)
return &response.ContentResponse{
Redirect: true,
URL: handler.signSourceURL(
ctx,
path,
thumb,
int64(model.GetIntSetting("preview_timeout", 60)),
),
}, nil
}
// Source 获取外链URL
func (handler *Driver) Source(
ctx context.Context,
path string,
baseURL url.URL,
ttl int64,
isDownload bool,
speed int,
) (string, error) {
func (handler *Driver) Source(ctx context.Context, path string, ttl int64, isDownload bool, speed int) (string, error) {
// 尝试从上下文获取文件名
fileName := ""
if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok {
+18 -12
View File
@@ -8,15 +8,18 @@ import (
"fmt"
"net/url"
"path"
"path/filepath"
"strings"
"time"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
"github.com/cloudreve/Cloudreve/v3/pkg/request"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
)
// Driver 远程存储策略适配器
@@ -121,7 +124,7 @@ func (handler *Driver) Get(ctx context.Context, path string) (response.RSCloser,
}
// 获取文件源地址
downloadURL, err := handler.Source(ctx, path, url.URL{}, 0, true, speedLimit)
downloadURL, err := handler.Source(ctx, path, 0, true, speedLimit)
if err != nil {
return nil, err
}
@@ -204,9 +207,19 @@ func (handler *Driver) Delete(ctx context.Context, files []string) ([]string, er
}
// Thumb 获取文件缩略图
func (handler *Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) {
sourcePath := base64.RawURLEncoding.EncodeToString([]byte(path))
thumbURL := handler.getAPIUrl("thumb") + "/" + sourcePath
func (handler *Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) {
// quick check by extension name
supported := []string{"png", "jpg", "jpeg", "gif"}
if len(handler.Policy.OptionsSerialized.ThumbExts) > 0 {
supported = handler.Policy.OptionsSerialized.ThumbExts
}
if !util.IsInExtensionList(supported, file.Name) {
return nil, driver.ErrorThumbNotSupported
}
sourcePath := base64.RawURLEncoding.EncodeToString([]byte(file.SourceName))
thumbURL := fmt.Sprintf("%s/%s/%s", handler.getAPIUrl("thumb"), sourcePath, filepath.Ext(file.Name))
ttl := model.GetIntSetting("preview_timeout", 60)
signedThumbURL, err := auth.SignURI(handler.AuthInstance, thumbURL, int64(ttl))
if err != nil {
@@ -220,14 +233,7 @@ func (handler *Driver) Thumb(ctx context.Context, path string) (*response.Conten
}
// Source 获取外链URL
func (handler *Driver) Source(
ctx context.Context,
path string,
baseURL url.URL,
ttl int64,
isDownload bool,
speed int,
) (string, error) {
func (handler *Driver) Source(ctx context.Context, path string, ttl int64, isDownload bool, speed int) (string, error) {
// 尝试从上下文获取文件名
fileName := "file"
if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok {
+28 -9
View File
@@ -3,12 +3,12 @@ package remote
import (
"context"
"errors"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
"github.com/cloudreve/Cloudreve/v3/pkg/mocks/remoteclientmock"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"testing"
@@ -50,7 +50,7 @@ func TestHandler_Source(t *testing.T) {
AuthInstance: auth.HMACAuth{},
}
ctx := context.Background()
res, err := handler.Source(ctx, "", url.URL{}, 0, true, 0)
res, err := handler.Source(ctx, "", 0, true, 0)
asserts.NoError(err)
asserts.NotEmpty(res)
}
@@ -65,7 +65,7 @@ func TestHandler_Source(t *testing.T) {
SourceName: "1.txt",
}
ctx := context.WithValue(context.Background(), fsctx.FileModelCtx, file)
res, err := handler.Source(ctx, "", url.URL{}, 10, true, 0)
res, err := handler.Source(ctx, "", 10, true, 0)
asserts.NoError(err)
asserts.Contains(res, "api/v3/slave/download/0")
}
@@ -80,7 +80,7 @@ func TestHandler_Source(t *testing.T) {
SourceName: "1.txt",
}
ctx := context.WithValue(context.Background(), fsctx.FileModelCtx, file)
res, err := handler.Source(ctx, "", url.URL{}, 10, true, 0)
res, err := handler.Source(ctx, "", 10, true, 0)
asserts.NoError(err)
asserts.Contains(res, "api/v3/slave/download/0")
asserts.Contains(res, "https://cqu.edu.cn")
@@ -96,7 +96,7 @@ func TestHandler_Source(t *testing.T) {
SourceName: "1.txt",
}
ctx := context.WithValue(context.Background(), fsctx.FileModelCtx, file)
res, err := handler.Source(ctx, "", url.URL{}, 10, true, 0)
res, err := handler.Source(ctx, "", 10, true, 0)
asserts.Error(err)
asserts.Empty(res)
}
@@ -111,7 +111,7 @@ func TestHandler_Source(t *testing.T) {
SourceName: "1.txt",
}
ctx := context.WithValue(context.Background(), fsctx.FileModelCtx, file)
res, err := handler.Source(ctx, "", url.URL{}, 10, false, 0)
res, err := handler.Source(ctx, "", 10, false, 0)
asserts.NoError(err)
asserts.Contains(res, "api/v3/slave/source/0")
}
@@ -373,14 +373,33 @@ func TestHandler_Thumb(t *testing.T) {
Type: "remote",
SecretKey: "test",
Server: "http://test.com",
OptionsSerialized: model.PolicyOption{
ThumbExts: []string{"txt"},
},
},
AuthInstance: auth.HMACAuth{},
}
file := &model.File{
Name: "1.txt",
SourceName: "1.txt",
}
ctx := context.Background()
asserts.NoError(cache.Set("setting_preview_timeout", "60", 0))
resp, err := handler.Thumb(ctx, "/1.txt")
asserts.NoError(err)
asserts.True(resp.Redirect)
// no error
{
resp, err := handler.Thumb(ctx, file)
asserts.NoError(err)
asserts.True(resp.Redirect)
}
// ext not support
{
file.Name = "1.jpg"
resp, err := handler.Thumb(ctx, file)
asserts.ErrorIs(err, driver.ErrorThumbNotSupported)
asserts.Nil(resp)
}
}
func TestHandler_Token(t *testing.T) {
+10 -22
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
"io"
"net/http"
"net/url"
@@ -163,14 +164,7 @@ func (handler *Driver) List(ctx context.Context, base string, recursive bool) ([
// Get 获取文件
func (handler *Driver) Get(ctx context.Context, path string) (response.RSCloser, error) {
// 获取文件源地址
downloadURL, err := handler.Source(
ctx,
path,
url.URL{},
int64(model.GetIntSetting("preview_timeout", 60)),
false,
0,
)
downloadURL, err := handler.Source(ctx, path, int64(model.GetIntSetting("preview_timeout", 60)), false, 0)
if err != nil {
return nil, err
}
@@ -257,26 +251,19 @@ func (handler *Driver) Delete(ctx context.Context, files []string) ([]string, er
for _, deleteRes := range res.Deleted {
deleted = append(deleted, *deleteRes.Key)
}
failed = util.SliceDifference(failed, deleted)
failed = util.SliceDifference(files, deleted)
return failed, nil
}
// Thumb 获取文件缩略图
func (handler *Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) {
return nil, errors.New("未实现")
func (handler *Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) {
return nil, driver.ErrorThumbNotSupported
}
// Source 获取外链URL
func (handler *Driver) Source(
ctx context.Context,
path string,
baseURL url.URL,
ttl int64,
isDownload bool,
speed int,
) (string, error) {
func (handler *Driver) Source(ctx context.Context, path string, ttl int64, isDownload bool, speed int) (string, error) {
// 尝试从上下文获取文件名
fileName := ""
@@ -339,9 +326,10 @@ func (handler *Driver) Token(ctx context.Context, ttl int64, uploadSession *seri
// 创建分片上传
expires := time.Now().Add(time.Duration(ttl) * time.Second)
res, err := handler.svc.CreateMultipartUpload(&s3.CreateMultipartUploadInput{
Bucket: &handler.Policy.BucketName,
Key: &fileInfo.SavePath,
Expires: &expires,
Bucket: &handler.Policy.BucketName,
Key: &fileInfo.SavePath,
Expires: &expires,
ContentType: aws.String(fileInfo.DetectMimeType()),
})
if err != nil {
return nil, fmt.Errorf("failed to create multipart upload: %w", err)
@@ -8,7 +8,6 @@ import (
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"net/url"
)
// Driver 影子存储策略,用于在从机端上传文件
@@ -39,11 +38,11 @@ func (d *Driver) Get(ctx context.Context, path string) (response.RSCloser, error
return nil, ErrNotImplemented
}
func (d *Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) {
func (d *Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) {
return nil, ErrNotImplemented
}
func (d *Driver) Source(ctx context.Context, path string, url url.URL, ttl int64, isDownload bool, speed int) (string, error) {
func (d *Driver) Source(ctx context.Context, path string, ttl int64, isDownload bool, speed int) (string, error) {
return "", ErrNotImplemented
}
@@ -102,11 +102,11 @@ func (d *Driver) Get(ctx context.Context, path string) (response.RSCloser, error
return nil, ErrNotImplemented
}
func (d *Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) {
func (d *Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) {
return nil, ErrNotImplemented
}
func (d *Driver) Source(ctx context.Context, path string, url url.URL, ttl int64, isDownload bool, speed int) (string, error) {
func (d *Driver) Source(ctx context.Context, path string, ttl int64, isDownload bool, speed int) (string, error) {
return "", ErrNotImplemented
}
+20 -26
View File
@@ -18,10 +18,12 @@ import (
"time"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
"github.com/cloudreve/Cloudreve/v3/pkg/request"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/upyun/go-sdk/upyun"
)
@@ -105,14 +107,7 @@ func (handler Driver) List(ctx context.Context, base string, recursive bool) ([]
// Get 获取文件
func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser, error) {
// 获取文件源地址
downloadURL, err := handler.Source(
ctx,
path,
url.URL{},
int64(model.GetIntSetting("preview_timeout", 60)),
false,
0,
)
downloadURL, err := handler.Source(ctx, path, int64(model.GetIntSetting("preview_timeout", 60)), false, 0)
if err != nil {
return nil, err
}
@@ -220,7 +215,18 @@ func (handler Driver) Delete(ctx context.Context, files []string) ([]string, err
}
// Thumb 获取文件缩略图
func (handler Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) {
func (handler Driver) Thumb(ctx context.Context, file *model.File) (*response.ContentResponse, error) {
// quick check by extension name
// https://help.upyun.com/knowledge-base/image/
supported := []string{"png", "jpg", "jpeg", "gif", "bmp", "webp", "svg"}
if len(handler.Policy.OptionsSerialized.ThumbExts) > 0 {
supported = handler.Policy.OptionsSerialized.ThumbExts
}
if !util.IsInExtensionList(supported, file.Name) {
return nil, driver.ErrorThumbNotSupported
}
var (
thumbSize = [2]uint{400, 300}
ok = false
@@ -229,15 +235,10 @@ func (handler Driver) Thumb(ctx context.Context, path string) (*response.Content
return nil, errors.New("failed to get thumbnail size")
}
thumbParam := fmt.Sprintf("!/fwfh/%dx%d", thumbSize[0], thumbSize[1])
thumbURL, err := handler.Source(
ctx,
path+thumbParam,
url.URL{},
int64(model.GetIntSetting("preview_timeout", 60)),
false,
0,
)
thumbEncodeQuality := model.GetIntSetting("thumb_encode_quality", 85)
thumbParam := fmt.Sprintf("!/fwfh/%dx%d/quality/%d", thumbSize[0], thumbSize[1], thumbEncodeQuality)
thumbURL, err := handler.Source(ctx, file.SourceName+thumbParam, int64(model.GetIntSetting("preview_timeout", 60)), false, 0)
if err != nil {
return nil, err
}
@@ -249,14 +250,7 @@ func (handler Driver) Thumb(ctx context.Context, path string) (*response.Content
}
// Source 获取外链URL
func (handler Driver) Source(
ctx context.Context,
path string,
baseURL url.URL,
ttl int64,
isDownload bool,
speed int,
) (string, error) {
func (handler Driver) Source(ctx context.Context, path string, ttl int64, isDownload bool, speed int) (string, error) {
// 尝试从上下文获取文件名
fileName := ""
if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok {
+15 -11
View File
@@ -64,10 +64,6 @@ func (fs *FileSystem) AddFile(ctx context.Context, parent *model.Folder, file fs
UploadSessionID: uploadInfo.UploadSessionID,
}
if fs.Policy.IsThumbExist(uploadInfo.FileName) {
newFile.PicInfo = "1,1"
}
err = newFile.Create()
if err != nil {
@@ -97,9 +93,10 @@ func (fs *FileSystem) GetPhysicalFileContent(ctx context.Context, path string) (
}
// Preview 预览文件
// path - 文件虚拟路径
// isText - 是否为文本文件,文本文件会忽略重定向,直接由
// 服务端拉取中转给用户,故会对文件大小进行限制
//
// path - 文件虚拟路径
// isText - 是否为文本文件,文本文件会忽略重定向,直接由
// 服务端拉取中转给用户,故会对文件大小进行限制
func (fs *FileSystem) Preview(ctx context.Context, id uint, isText bool) (*response.ContentResponse, error) {
err := fs.resetFileIDIfNotExist(ctx, id)
if err != nil {
@@ -174,6 +171,7 @@ func (fs *FileSystem) deleteGroupedFile(ctx context.Context, files map[uint][]*m
// 失败的文件列表
// TODO 并行删除
failed := make(map[uint][]string, len(files))
thumbs := make([]string, 0)
for policyID, toBeDeletedFiles := range files {
// 列举出需要物理删除的文件的物理路径
@@ -188,7 +186,11 @@ func (fs *FileSystem) deleteGroupedFile(ctx context.Context, files map[uint][]*m
uploadSession := session.(serializer.UploadSession)
uploadSessions = append(uploadSessions, &uploadSession)
}
}
// Check if sidecar thumb file exist
if model.IsTrueVal(toBeDeletedFiles[i].MetadataSerialized[model.ThumbSidecarMetadataKey]) {
thumbs = append(thumbs, toBeDeletedFiles[i].ThumbFile())
}
}
@@ -210,8 +212,11 @@ func (fs *FileSystem) deleteGroupedFile(ctx context.Context, files map[uint][]*m
}
// 执行删除
failedFile, _ := fs.Handler.Delete(ctx, sourceNamesAll)
failed[policyID] = failedFile
toBeDeletedSrcs := append(sourceNamesAll, thumbs...)
failedFile, _ := fs.Handler.Delete(ctx, toBeDeletedSrcs)
// Exclude failed results related to thumb file
failed[policyID] = util.SliceDifference(failedFile, thumbs)
}
return failed
@@ -295,8 +300,7 @@ func (fs *FileSystem) SignURL(ctx context.Context, file *model.File, ttl int64,
// 签名最终URL
// 生成外链地址
siteURL := model.GetSiteURL()
source, err := fs.Handler.Source(ctx, fs.FileTarget[0].SourceName, *siteURL, ttl, isDownload, fs.User.Group.SpeedLimit)
source, err := fs.Handler.Source(ctx, fs.FileTarget[0].SourceName, ttl, isDownload, fs.User.Group.SpeedLimit)
if err != nil {
return "", serializer.NewError(serializer.CodeNotSet, "Failed to get source link", err)
}
+13 -3
View File
@@ -58,7 +58,6 @@ func TestFileSystem_AddFile(t *testing.T) {
asserts.NoError(err)
asserts.NoError(mock.ExpectationsWereMet())
asserts.Equal("/Uploads/1_sad.png", f.SourceName)
asserts.NotEmpty(f.PicInfo)
// 前置钩子执行失败
{
@@ -312,6 +311,19 @@ func TestFileSystem_deleteGroupedFile(t *testing.T) {
_, ok := cache.Get(UploadSessionCachePrefix + sessionID)
asserts.False(ok)
}
// 包含缩略图
{
files[0].MetadataSerialized = map[string]string{
model.ThumbSidecarMetadataKey: "1",
}
failed := fs.deleteGroupedFile(ctx, fs.GroupFileByPolicy(ctx, files))
asserts.Equal(map[uint][]string{
1: {},
2: {},
3: {},
}, failed)
}
}
func TestFileSystem_GetSource(t *testing.T) {
@@ -340,8 +352,6 @@ func TestFileSystem_GetSource(t *testing.T) {
sqlmock.NewRows([]string{"id", "type", "is_origin_link_enable"}).
AddRow(35, "local", true),
)
// 查找站点URL
mock.ExpectQuery("SELECT(.+)").WithArgs("siteURL").WillReturnRows(sqlmock.NewRows([]string{"id", "value"}).AddRow(1, "https://cloudreve.org"))
sourceURL, err := fs.GetSource(ctx, 2)
asserts.NoError(mock.ExpectationsWereMet())
+5
View File
@@ -8,6 +8,7 @@ import (
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/cos"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/googledrive"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/local"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/onedrive"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/oss"
@@ -176,6 +177,10 @@ func (fs *FileSystem) DispatchHandler() error {
handler, err := s3.NewDriver(currentPolicy)
fs.Handler = handler
return err
case "googledrive":
handler, err := googledrive.NewDriver(currentPolicy)
fs.Handler = handler
return err
default:
return ErrUnknownPolicyType
}
+6
View File
@@ -35,4 +35,10 @@ const (
CancelFuncCtx
// 文件在从机节点中的路径
SlaveSrcPath
// Webdav目标名称
WebdavDstName
// WebDAVCtx WebDAV
WebDAVCtx
// WebDAV反代Url
WebDAVProxyUrlCtx
)
+13 -3
View File
@@ -2,6 +2,7 @@ package fsctx
import (
"errors"
"github.com/HFO4/aliyun-oss-go-sdk/oss"
"io"
"time"
)
@@ -17,7 +18,7 @@ const (
type UploadTaskInfo struct {
Size uint64
MIMEType string
MimeType string
FileName string
VirtualPath string
Mode WriteMode
@@ -30,6 +31,15 @@ type UploadTaskInfo struct {
Src string
}
// Get mimetype of uploaded file, if it's not defined, detect it from file name
func (u *UploadTaskInfo) DetectMimeType() string {
if u.MimeType != "" {
return u.MimeType
}
return oss.TypeByExtension(u.FileName)
}
// FileHeader 上传来的文件数据处理器
type FileHeader interface {
io.Reader
@@ -51,7 +61,7 @@ type FileStream struct {
Size uint64
VirtualPath string
Name string
MIMEType string
MimeType string
SavePath string
UploadSessionID *string
AppendStart uint64
@@ -90,7 +100,7 @@ func (file *FileStream) Seekable() bool {
func (file *FileStream) Info() *UploadTaskInfo {
return &UploadTaskInfo{
Size: file.Size,
MIMEType: file.MIMEType,
MimeType: file.MimeType,
FileName: file.Name,
VirtualPath: file.VirtualPath,
Mode: file.Mode,
+35 -32
View File
@@ -10,7 +10,10 @@ import (
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"io/ioutil"
"net/http"
"strconv"
"strings"
"time"
)
// Hook 钩子函数
@@ -177,24 +180,12 @@ func GenericAfterUpdate(ctx context.Context, fs *FileSystem, newFile fsctx.FileH
// SlaveAfterUpload Slave模式下上传完成钩子
func SlaveAfterUpload(session *serializer.UploadSession) Hook {
return func(ctx context.Context, fs *FileSystem, fileHeader fsctx.FileHeader) error {
fileInfo := fileHeader.Info()
// 构造一个model.File,用于生成缩略图
file := model.File{
Name: fileInfo.FileName,
SourceName: fileInfo.SavePath,
}
fs.GenerateThumbnail(ctx, &file)
if session.Callback == "" {
return nil
}
// 发送回调请求
callbackBody := serializer.UploadCallback{
PicInfo: file.PicInfo,
}
callbackBody := serializer.UploadCallback{}
return cluster.RemoteCallback(session.Callback, callbackBody)
}
}
@@ -231,21 +222,6 @@ func GenericAfterUpload(ctx context.Context, fs *FileSystem, fileHeader fsctx.Fi
return nil
}
// HookGenerateThumb 生成缩略图
func HookGenerateThumb(ctx context.Context, fs *FileSystem, fileHeader fsctx.FileHeader) error {
// 异步尝试生成缩略图
fileMode := fileHeader.Info().Model.(*model.File)
if fs.Policy.IsThumbGenerateNeeded() {
fs.recycleLock.Lock()
go func() {
defer fs.recycleLock.Unlock()
_, _ = fs.Handler.Delete(ctx, []string{fileMode.SourceName + model.GetSettingByNameWithDefault("thumb_file_suffix", "._thumb")})
fs.GenerateThumbnail(ctx, fileMode)
}()
}
return nil
}
// HookClearFileHeaderSize 将FileHeader大小设定为0
func HookClearFileHeaderSize(ctx context.Context, fs *FileSystem, fileHeader fsctx.FileHeader) error {
fileHeader.SetSize(0)
@@ -284,10 +260,6 @@ func HookPopPlaceholderToFile(picInfo string) Hook {
return func(ctx context.Context, fs *FileSystem, fileHeader fsctx.FileHeader) error {
fileInfo := fileHeader.Info()
fileModel := fileInfo.Model.(*model.File)
if picInfo == "" && fs.Policy.IsThumbExist(fileInfo.FileName) {
picInfo = "1,1"
}
return fileModel.PopChunkToFile(fileInfo.LastModified, picInfo)
}
}
@@ -299,3 +271,34 @@ func HookDeleteUploadSession(id string) Hook {
return nil
}
}
// NewWebdavAfterUploadHook 每次创建一个新的钩子函数 rclone 在 PUT 请求里有 OC-Checksum 字符串
// 和 X-OC-Mtime
func NewWebdavAfterUploadHook(request *http.Request) func(ctx context.Context, fs *FileSystem, newFile fsctx.FileHeader) error {
var modtime time.Time
if timeVal := request.Header.Get("X-OC-Mtime"); timeVal != "" {
timeUnix, err := strconv.ParseInt(timeVal, 10, 64)
if err == nil {
modtime = time.Unix(timeUnix, 0)
}
}
checksum := request.Header.Get("OC-Checksum")
return func(ctx context.Context, fs *FileSystem, newFile fsctx.FileHeader) error {
file := newFile.Info().Model.(*model.File)
if !modtime.IsZero() {
err := model.DB.Model(file).UpdateColumn("updated_at", modtime).Error
if err != nil {
return err
}
}
if checksum != "" {
return file.UpdateMetadata(map[string]string{
model.ChecksumMetadataKey: checksum,
})
}
return nil
}
}
+29 -27
View File
@@ -360,7 +360,7 @@ func TestHookClearFileSize(t *testing.T) {
)
mock.ExpectBegin()
mock.ExpectExec("UPDATE(.+)files(.+)").
WithArgs(0, sqlmock.AnyArg(), 1, 10).
WithArgs("", 0, sqlmock.AnyArg(), 1, 10).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("UPDATE(.+)users(.+)").
WithArgs(10, sqlmock.AnyArg()).
@@ -394,7 +394,7 @@ func TestHookUpdateSourceName(t *testing.T) {
}
ctx := context.WithValue(context.Background(), fsctx.FileModelCtx, originFile)
mock.ExpectBegin()
mock.ExpectExec("UPDATE(.+)").WithArgs("new.txt", sqlmock.AnyArg(), 1).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("UPDATE(.+)").WithArgs("", "new.txt", sqlmock.AnyArg(), 1).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
err := HookUpdateSourceName(ctx, fs, nil)
asserts.NoError(mock.ExpectationsWereMet())
@@ -429,7 +429,7 @@ func TestGenericAfterUpdate(t *testing.T) {
fs.Handler = handlerMock
mock.ExpectBegin()
mock.ExpectExec("UPDATE(.+)files(.+)").
WithArgs(10, sqlmock.AnyArg(), 1, 0).
WithArgs("", 10, sqlmock.AnyArg(), 1, 0).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("UPDATE(.+)users(.+)").
WithArgs(10, sqlmock.AnyArg()).
@@ -462,7 +462,7 @@ func TestGenericAfterUpdate(t *testing.T) {
mock.ExpectBegin()
mock.ExpectExec("UPDATE(.+)").
WithArgs(10, sqlmock.AnyArg(), 1, 0).
WithArgs("", 10, sqlmock.AnyArg(), 1, 0).
WillReturnError(errors.New("error"))
mock.ExpectRollback()
@@ -473,27 +473,6 @@ func TestGenericAfterUpdate(t *testing.T) {
}
}
func TestHookGenerateThumb(t *testing.T) {
a := assert.New(t)
mockHandler := &FileHeaderMock{}
fs := &FileSystem{
User: &model.User{
Model: gorm.Model{ID: 1},
},
Handler: mockHandler,
Policy: &model.Policy{Type: "local"},
}
mockHandler.On("Delete", testMock.Anything, []string{"1.txt._thumb"}).Return([]string{}, nil)
a.NoError(HookGenerateThumb(context.Background(), fs, &fsctx.FileStream{
Model: &model.File{
SourceName: "1.txt",
},
}))
fs.Recycle()
mockHandler.AssertExpectations(t)
}
func TestSlaveAfterUpload(t *testing.T) {
asserts := assert.New(t)
conf.SystemConfig.Mode = "slave"
@@ -625,7 +604,7 @@ func TestHookChunkUploaded(t *testing.T) {
}
mock.ExpectBegin()
mock.ExpectExec("UPDATE(.+)files(.+)").WithArgs(20, sqlmock.AnyArg(), 1, 0).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("UPDATE(.+)files(.+)").WithArgs("", 20, sqlmock.AnyArg(), 1, 0).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("UPDATE(.+)users(.+)").
WithArgs(20, sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(1, 1))
@@ -646,7 +625,7 @@ func TestHookChunkUploadFailed(t *testing.T) {
}
mock.ExpectBegin()
mock.ExpectExec("UPDATE(.+)files(.+)").WithArgs(10, sqlmock.AnyArg(), 1, 0).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("UPDATE(.+)files(.+)").WithArgs("", 10, sqlmock.AnyArg(), 1, 0).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("UPDATE(.+)users(.+)").
WithArgs(10, sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(1, 1))
@@ -704,3 +683,26 @@ func TestHookDeleteUploadSession(t *testing.T) {
_, ok := cache.Get(UploadSessionCachePrefix + "TestHookDeleteUploadSession")
a.False(ok)
}
func TestNewWebdavAfterUploadHook(t *testing.T) {
a := assert.New(t)
fs := &FileSystem{}
file := &fsctx.FileStream{
Model: &model.File{
Model: gorm.Model{ID: 1},
},
}
req, _ := http.NewRequest("get", "http://localhost", nil)
req.Header.Add("X-Oc-Mtime", "1681521402")
req.Header.Add("OC-Checksum", "checksum")
mock.ExpectBegin()
mock.ExpectExec("UPDATE(.+)files(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
mock.ExpectBegin()
mock.ExpectExec("UPDATE(.+)files(.+)").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
err := NewWebdavAfterUploadHook(req)(context.Background(), fs, file)
a.NoError(err)
a.NoError(mock.ExpectationsWereMet())
}
+119 -49
View File
@@ -2,13 +2,16 @@ package filesystem
import (
"context"
"errors"
"fmt"
"os"
"sync"
"runtime"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
"github.com/cloudreve/Cloudreve/v3/pkg/thumb"
@@ -20,28 +23,51 @@ import (
================
*/
// HandledExtension 可以生成缩略图的文件扩展名
var HandledExtension = []string{"jpg", "jpeg", "png", "gif"}
// GetThumb 获取文件的缩略图
func (fs *FileSystem) GetThumb(ctx context.Context, id uint) (*response.ContentResponse, error) {
// 根据 ID 查找文件
err := fs.resetFileIDIfNotExist(ctx, id)
if err != nil || fs.FileTarget[0].PicInfo == "" {
return &response.ContentResponse{
Redirect: false,
}, ErrObjectNotExist
if err != nil {
return nil, ErrObjectNotExist
}
file := fs.FileTarget[0]
if !file.ShouldLoadThumb() {
return nil, ErrObjectNotExist
}
w, h := fs.GenerateThumbnailSize(0, 0)
ctx = context.WithValue(ctx, fsctx.ThumbSizeCtx, [2]uint{w, h})
ctx = context.WithValue(ctx, fsctx.FileModelCtx, fs.FileTarget[0])
res, err := fs.Handler.Thumb(ctx, fs.FileTarget[0].SourceName)
// 本地存储策略出错时重新生成缩略图
if err != nil && fs.Policy.Type == "local" {
fs.GenerateThumbnail(ctx, &fs.FileTarget[0])
res, err = fs.Handler.Thumb(ctx, fs.FileTarget[0].SourceName)
ctx = context.WithValue(ctx, fsctx.FileModelCtx, file)
res, err := fs.Handler.Thumb(ctx, &file)
if errors.Is(err, driver.ErrorThumbNotExist) {
// Regenerate thumb if the thumb is not initialized yet
if generateErr := fs.generateThumbnail(ctx, &file); generateErr == nil {
res, err = fs.Handler.Thumb(ctx, &file)
} else {
err = generateErr
}
} else if errors.Is(err, driver.ErrorThumbNotSupported) {
// Policy handler explicitly indicates thumb not available, check if proxy is enabled
if fs.Policy.CouldProxyThumb() {
// if thumb id marked as existed, redirect to "sidecar" thumb file.
if file.MetadataSerialized != nil &&
file.MetadataSerialized[model.ThumbStatusMetadataKey] == model.ThumbStatusExist {
// redirect to sidecar file
res = &response.ContentResponse{
Redirect: true,
}
res.URL, err = fs.Handler.Source(ctx, file.ThumbFile(), int64(model.GetIntSetting("preview_timeout", 60)), false, 0)
} else {
// if not exist, generate and upload the sidecar thumb.
if err = fs.generateThumbnail(ctx, &file); err == nil {
return fs.GetThumb(ctx, id)
}
}
} else {
// thumb not supported and proxy is disabled, mark as not available
_ = updateThumbStatus(&file, model.ThumbStatusNotAvailable)
}
}
if err == nil && conf.SystemConfig.Mode == "master" {
@@ -84,65 +110,109 @@ func (pool *Pool) releaseWorker() {
<-pool.worker
}
// GenerateThumbnail 尝试为本地策略文件生成缩略图并获取图像原始大小
// TODO 失败时,如果之前还有图像信息,则清除
func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) {
// 判断是否可以生成缩略图
if !IsInExtensionList(HandledExtension, file.Name) {
return
}
// generateThumbnail generates thumb for given file, upload the thumb file back with given suffix
func (fs *FileSystem) generateThumbnail(ctx context.Context, file *model.File) error {
// 新建上下文
newCtx, cancel := context.WithCancel(context.Background())
defer cancel()
// TODO: check file size
if file.Size > uint64(model.GetIntSetting("thumb_max_src_size", 31457280)) {
_ = updateThumbStatus(file, model.ThumbStatusNotAvailable)
return errors.New("file too large")
}
getThumbWorker().addWorker()
defer getThumbWorker().releaseWorker()
// 获取文件数据
source, err := fs.Handler.Get(newCtx, file.SourceName)
if err != nil {
return
return fmt.Errorf("faield to fetch original file %q: %w", file.SourceName, err)
}
defer source.Close()
getThumbWorker().addWorker()
defer getThumbWorker().releaseWorker()
image, err := thumb.NewThumbFromFile(source, file.Name)
if err != nil {
util.Log().Warning("Cannot generate thumb because of failed to parse image %q: %s", file.SourceName, err)
return
// Provide file source path for local policy files
src := ""
if conf.SystemConfig.Mode == "slave" || file.GetPolicy().Type == "local" {
src = file.SourceName
}
// 获取原始图像尺寸
w, h := image.GetSize()
thumbRes, err := thumb.Generators.Generate(ctx, source, src, file.Name, model.GetSettingByNames(
"thumb_width",
"thumb_height",
"thumb_builtin_enabled",
"thumb_vips_enabled",
"thumb_ffmpeg_enabled",
"thumb_libreoffice_enabled",
))
if err != nil {
_ = updateThumbStatus(file, model.ThumbStatusNotAvailable)
return fmt.Errorf("failed to generate thumb for %q: %w", file.Name, err)
}
defer os.Remove(thumbRes.Path)
thumbFile, err := os.Open(thumbRes.Path)
if err != nil {
return fmt.Errorf("failed to open temp thumb %q: %w", thumbRes.Path, err)
}
defer thumbFile.Close()
fileInfo, err := thumbFile.Stat()
if err != nil {
return fmt.Errorf("failed to stat temp thumb %q: %w", thumbRes.Path, err)
}
if err = fs.Handler.Put(newCtx, &fsctx.FileStream{
Mode: fsctx.Overwrite,
File: thumbFile,
Seeker: thumbFile,
Size: uint64(fileInfo.Size()),
SavePath: file.SourceName + model.GetSettingByNameWithDefault("thumb_file_suffix", "._thumb"),
}); err != nil {
return fmt.Errorf("failed to save thumb for %q: %w", file.Name, err)
}
// 生成缩略图
image.GetThumb(fs.GenerateThumbnailSize(w, h))
// 保存到文件
err = image.Save(util.RelativePath(file.SourceName + model.GetSettingByNameWithDefault("thumb_file_suffix", "._thumb")))
image = nil
if model.IsTrueVal(model.GetSettingByName("thumb_gc_after_gen")) {
util.Log().Debug("GenerateThumbnail runtime.GC")
util.Log().Debug("generateThumbnail runtime.GC")
runtime.GC()
}
if err != nil {
util.Log().Warning("Failed to save thumb: %s", err)
return
}
// 更新文件的图像信息
if file.Model.ID > 0 {
err = file.UpdatePicInfo(fmt.Sprintf("%d,%d", w, h))
} else {
file.PicInfo = fmt.Sprintf("%d,%d", w, h)
}
// Mark this file as thumb available
err = updateThumbStatus(file, model.ThumbStatusExist)
// 失败时删除缩略图文件
if err != nil {
_, _ = fs.Handler.Delete(newCtx, []string{file.SourceName + model.GetSettingByNameWithDefault("thumb_file_suffix", "._thumb")})
}
return nil
}
// GenerateThumbnailSize 获取要生成的缩略图的尺寸
func (fs *FileSystem) GenerateThumbnailSize(w, h int) (uint, uint) {
return uint(model.GetIntSetting("thumb_width", 400)), uint(model.GetIntSetting("thumb_width", 300))
return uint(model.GetIntSetting("thumb_width", 400)), uint(model.GetIntSetting("thumb_height", 300))
}
func updateThumbStatus(file *model.File, status string) error {
if file.Model.ID > 0 {
meta := map[string]string{
model.ThumbStatusMetadataKey: status,
}
if status == model.ThumbStatusExist {
meta[model.ThumbSidecarMetadataKey] = "true"
}
return file.UpdateMetadata(meta)
} else {
if file.MetadataSerialized == nil {
file.MetadataSerialized = map[string]string{}
}
file.MetadataSerialized[model.ThumbStatusMetadataKey] = status
}
return nil
}
+93 -36
View File
@@ -5,8 +5,10 @@ import (
"errors"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
"github.com/cloudreve/Cloudreve/v3/pkg/request"
"github.com/cloudreve/Cloudreve/v3/pkg/mocks/thumbmock"
"github.com/cloudreve/Cloudreve/v3/pkg/thumb"
testMock "github.com/stretchr/testify/mock"
"testing"
@@ -14,30 +16,104 @@ import (
)
func TestFileSystem_GetThumb(t *testing.T) {
asserts := assert.New(t)
a := assert.New(t)
fs := &FileSystem{User: &model.User{}}
// 非图像文件
// file not found
{
fs.SetTargetFile(&[]model.File{{}})
_, err := fs.GetThumb(context.Background(), 1)
asserts.Equal(err, ErrObjectNotExist)
mock.ExpectQuery("SELECT(.+)").WillReturnError(errors.New("error"))
res, err := fs.GetThumb(context.Background(), 1)
a.ErrorIs(err, ErrObjectNotExist)
a.Nil(res)
a.NoError(mock.ExpectationsWereMet())
}
// 成功
// thumb not exist
{
cache.Set("setting_thumb_width", "10", 0)
cache.Set("setting_thumb_height", "10", 0)
cache.Set("setting_preview_timeout", "50", 0)
testHandller2 := new(FileHeaderMock)
testHandller2.On("Thumb", testMock.Anything, "").Return(&response.ContentResponse{}, nil)
fs.CleanTargets()
fs.SetTargetFile(&[]model.File{{PicInfo: "1,1", Policy: model.Policy{Type: "mock"}}})
fs.SetTargetFile(&[]model.File{{
MetadataSerialized: map[string]string{
model.ThumbStatusMetadataKey: model.ThumbStatusNotAvailable,
},
Policy: model.Policy{Type: "mock"},
}})
fs.FileTarget[0].Policy.ID = 1
fs.Handler = testHandller2
res, err := fs.GetThumb(context.Background(), 1)
asserts.NoError(err)
asserts.EqualValues(50, res.MaxAge)
a.ErrorIs(err, ErrObjectNotExist)
a.Nil(res)
}
// thumb not initialized, also failed to generate
{
fs.CleanTargets()
fs.SetTargetFile(&[]model.File{{
Policy: model.Policy{Type: "mock"},
Size: 31457281,
}})
testHandller2 := new(FileHeaderMock)
testHandller2.On("Thumb", testMock.Anything, &fs.FileTarget[0]).Return(&response.ContentResponse{}, driver.ErrorThumbNotExist)
fs.Handler = testHandller2
fs.FileTarget[0].Policy.ID = 1
res, err := fs.GetThumb(context.Background(), 1)
a.Contains(err.Error(), "file too large")
a.Nil(res.Content)
}
// thumb not initialized, failed to get source
{
fs.CleanTargets()
fs.SetTargetFile(&[]model.File{{
Policy: model.Policy{Type: "mock"},
}})
testHandller2 := new(FileHeaderMock)
testHandller2.On("Thumb", testMock.Anything, &fs.FileTarget[0]).Return(&response.ContentResponse{}, driver.ErrorThumbNotExist)
testHandller2.On("Get", testMock.Anything, "").Return(MockRSC{}, errors.New("error"))
fs.Handler = testHandller2
fs.FileTarget[0].Policy.ID = 1
res, err := fs.GetThumb(context.Background(), 1)
a.Contains(err.Error(), "error")
a.Nil(res.Content)
}
// thumb not initialized, no available generators
{
thumb.Generators = []thumb.Generator{}
fs.CleanTargets()
fs.SetTargetFile(&[]model.File{{
Policy: model.Policy{Type: "local"},
}})
testHandller2 := new(FileHeaderMock)
testHandller2.On("Thumb", testMock.Anything, &fs.FileTarget[0]).Return(&response.ContentResponse{}, driver.ErrorThumbNotExist)
testHandller2.On("Get", testMock.Anything, "").Return(MockRSC{}, nil)
fs.Handler = testHandller2
fs.FileTarget[0].Policy.ID = 1
res, err := fs.GetThumb(context.Background(), 1)
a.ErrorIs(err, thumb.ErrNotAvailable)
a.Nil(res)
}
// thumb not initialized, thumb generated but cannot be open
{
mockGenerator := &thumbmock.GeneratorMock{}
thumb.Generators = []thumb.Generator{mockGenerator}
fs.CleanTargets()
fs.SetTargetFile(&[]model.File{{
Policy: model.Policy{Type: "mock"},
}})
cache.Set("setting_thumb_vips_enabled", "1", 0)
testHandller2 := new(FileHeaderMock)
testHandller2.On("Thumb", testMock.Anything, &fs.FileTarget[0]).Return(&response.ContentResponse{}, driver.ErrorThumbNotExist)
testHandller2.On("Get", testMock.Anything, "").Return(MockRSC{}, nil)
mockGenerator.On("Generate", testMock.Anything, testMock.Anything, testMock.Anything, testMock.Anything, testMock.Anything).
Return(&thumb.Result{Path: "not_exit_thumb"}, nil)
fs.Handler = testHandller2
fs.FileTarget[0].Policy.ID = 1
res, err := fs.GetThumb(context.Background(), 1)
a.Contains(err.Error(), "failed to open temp thumb")
a.Nil(res.Content)
testHandller2.AssertExpectations(t)
mockGenerator.AssertExpectations(t)
}
}
@@ -49,22 +125,3 @@ func TestFileSystem_ThumbWorker(t *testing.T) {
getThumbWorker().releaseWorker()
})
}
func TestFileSystem_GenerateThumbnail(t *testing.T) {
fs := &FileSystem{User: &model.User{}}
// 无法生成缩略图
{
fs.SetTargetFile(&[]model.File{{}})
fs.GenerateThumbnail(context.Background(), &model.File{})
}
// 无法获取文件数据
{
testHandller := new(FileHeaderMock)
testHandller.On("Get", testMock.Anything, "").Return(request.NopRSCloser{}, errors.New("error"))
fs.Handler = testHandller
fs.GenerateThumbnail(context.Background(), &model.File{Name: "test.png"})
testHandller.AssertExpectations(t)
}
}
+18 -5
View File
@@ -69,6 +69,11 @@ func (fs *FileSystem) Copy(ctx context.Context, dirs, files []uint, src, dst str
// 记录复制的文件的总容量
var newUsedStorage uint64
// 设置webdav目标名
if dstName, ok := ctx.Value(fsctx.WebdavDstName).(string); ok {
dstFolder.WebdavDstName = dstName
}
// 复制目录
if len(dirs) > 0 {
subFileSizes, err := srcFolder.CopyFolderTo(dirs[0], dstFolder)
@@ -103,6 +108,11 @@ func (fs *FileSystem) Move(ctx context.Context, dirs, files []uint, src, dst str
return ErrPathNotExist
}
// 设置webdav目标名
if dstName, ok := ctx.Value(fsctx.WebdavDstName).(string); ok {
dstFolder.WebdavDstName = dstName
}
// 处理目录及子文件移动
err := srcFolder.MoveFolderTo(dirs, dstFolder)
if err != nil {
@@ -120,8 +130,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 +166,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++ {
@@ -337,7 +351,6 @@ func (fs *FileSystem) listObjects(ctx context.Context, parent string, files []mo
ID: hashid.HashID(subFolder.ID, hashid.FolderID),
Name: subFolder.Name,
Path: processedPath,
Pic: "",
Size: 0,
Type: "dir",
Date: subFolder.UpdatedAt,
@@ -359,7 +372,7 @@ func (fs *FileSystem) listObjects(ctx context.Context, parent string, files []mo
ID: hashid.HashID(file.ID, hashid.FileID),
Name: file.Name,
Path: processedPath,
Pic: file.PicInfo,
Thumb: file.ShouldLoadThumb(),
Size: file.Size,
Type: "file",
Date: file.UpdatedAt,
+5 -7
View File
@@ -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)
}
@@ -684,7 +682,7 @@ func TestFileSystem_Rename(t *testing.T) {
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(10, "old.text"))
mock.ExpectBegin()
mock.ExpectExec("UPDATE(.+)files(.+)SET(.+)").
WithArgs("new.txt", 10).
WithArgs(sqlmock.AnyArg(), "new.txt", sqlmock.AnyArg(), 10).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
err := fs.Rename(ctx, []uint{}, []uint{10}, "new.txt")
@@ -710,7 +708,7 @@ func TestFileSystem_Rename(t *testing.T) {
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(10, "old.text"))
mock.ExpectBegin()
mock.ExpectExec("UPDATE(.+)files(.+)SET(.+)").
WithArgs("new.txt", 10).
WithArgs(sqlmock.AnyArg(), "new.txt", sqlmock.AnyArg(), 10).
WillReturnError(errors.New("error"))
mock.ExpectRollback()
err := fs.Rename(ctx, []uint{}, []uint{10}, "new.txt")
@@ -1,4 +1,4 @@
package onedrive
package oauth
import "sync"
+8
View File
@@ -0,0 +1,8 @@
package oauth
import "context"
type TokenProvider interface {
UpdateCredential(ctx context.Context, isSlave bool) error
AccessToken() string
}
-1
View File
@@ -210,7 +210,6 @@ func (fs *FileSystem) UploadFromStream(ctx context.Context, file *fsctx.FileStre
fs.Use("BeforeUpload", HookValidateCapacity)
fs.Use("AfterUploadCanceled", HookDeleteTempFile)
fs.Use("AfterUpload", GenericAfterUpload)
fs.Use("AfterUpload", HookGenerateThumb)
fs.Use("AfterValidateFailed", HookDeleteTempFile)
}
fs.Lock.Unlock()
+3 -4
View File
@@ -16,7 +16,6 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
)
@@ -55,13 +54,13 @@ func (m FileHeaderMock) Delete(ctx context.Context, files []string) ([]string, e
return args.Get(0).([]string), args.Error(1)
}
func (m FileHeaderMock) Thumb(ctx context.Context, files string) (*response.ContentResponse, error) {
func (m FileHeaderMock) Thumb(ctx context.Context, files *model.File) (*response.ContentResponse, error) {
args := m.Called(ctx, files)
return args.Get(0).(*response.ContentResponse), args.Error(1)
}
func (m FileHeaderMock) Source(ctx context.Context, path string, url url.URL, expires int64, isDownload bool, speed int) (string, error) {
args := m.Called(ctx, path, url, expires, isDownload, speed)
func (m FileHeaderMock) Source(ctx context.Context, path string, expires int64, isDownload bool, speed int) (string, error) {
args := m.Called(ctx, path, expires, isDownload, speed)
return args.Get(0).(string), args.Error(1)
}
+1 -17
View File
@@ -2,7 +2,6 @@ package filesystem
import (
"context"
"path/filepath"
"strings"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
@@ -63,20 +62,5 @@ func (fs *FileSystem) ValidateExtension(ctx context.Context, fileName string) bo
return true
}
return IsInExtensionList(fs.Policy.OptionsSerialized.FileType, fileName)
}
// IsInExtensionList 返回文件的扩展名是否在给定的列表范围内
func IsInExtensionList(extList []string, fileName string) bool {
ext := strings.ToLower(filepath.Ext(fileName))
// 无扩展名时
if len(ext) == 0 {
return false
}
if util.ContainsString(extList, ext[1:]) {
return true
}
return false
return util.IsInExtensionList(fs.Policy.OptionsSerialized.FileType, fileName)
}
+37
View File
@@ -0,0 +1,37 @@
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)
}
func (c CacheClientMock) Persist(path string) error {
return c.Called(path).Error(0)
}
func (c CacheClientMock) Restore(path string) error {
return c.Called(path).Error(0)
}
+1 -1
View File
@@ -37,7 +37,7 @@ func (s SlaveControllerMock) GetMasterInfo(s2 string) (*cluster.MasterInfo, erro
return args.Get(0).(*cluster.MasterInfo), args.Error(1)
}
func (s SlaveControllerMock) GetOneDriveToken(s2 string, u uint) (string, error) {
func (s SlaveControllerMock) GetPolicyOauthToken(s2 string, u uint) (string, error) {
args := s.Called(s2, u)
return args.String(0), args.Error(1)
}
+25
View File
@@ -0,0 +1,25 @@
package thumbmock
import (
"context"
"github.com/cloudreve/Cloudreve/v3/pkg/thumb"
"github.com/stretchr/testify/mock"
"io"
)
type GeneratorMock struct {
mock.Mock
}
func (g GeneratorMock) Generate(ctx context.Context, file io.Reader, src string, name string, options map[string]string) (*thumb.Result, error) {
res := g.Called(ctx, file, src, name, options)
return res.Get(0).(*thumb.Result), res.Error(1)
}
func (g GeneratorMock) Priority() int {
return 0
}
func (g GeneratorMock) EnableFlag() string {
return "thumb_vips_enabled"
}
+21
View File
@@ -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 uint, 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)
}
+47 -1
View File
@@ -36,7 +36,7 @@ type Object struct {
ID string `json:"id"`
Name string `json:"name"`
Path string `json:"path"`
Pic string `json:"pic"`
Thumb bool `json:"thumb"`
Size uint64 `json:"size"`
Type string `json:"type"`
Date time.Time `json:"date"`
@@ -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
}
+19 -17
View File
@@ -7,22 +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"`
AppPromotion bool `json:"app_promotion"`
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 {
@@ -60,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)
@@ -85,6 +86,7 @@ func BuildSiteConfig(settings map[string]string, user *model.User) Response {
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
}
+3 -3
View File
@@ -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)
}
+4
View File
@@ -41,6 +41,8 @@ type group struct {
CompressEnabled bool `json:"compress"`
WebDAVEnabled bool `json:"webdav"`
SourceBatchSize int `json:"sourceBatch"`
AdvanceDelete bool `json:"advanceDelete"`
AllowWebDAVProxy bool `json:"allowWebDAVProxy"`
}
type tag struct {
@@ -99,7 +101,9 @@ func BuildUser(user model.User) User {
ShareDownload: user.Group.OptionsSerialized.ShareDownload,
CompressEnabled: user.Group.OptionsSerialized.ArchiveTask,
WebDAVEnabled: user.Group.WebDAVEnabled,
AllowWebDAVProxy: user.Group.OptionsSerialized.WebDAVProxy,
SourceBatchSize: user.Group.OptionsSerialized.SourceBatchSize,
AdvanceDelete: user.Group.OptionsSerialized.AdvanceDelete,
},
Tags: buildTagRes(tags),
}
+136
View File
@@ -0,0 +1,136 @@
package sessionstore
import (
"bytes"
"encoding/base32"
"encoding/gob"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
"net/http"
"strings"
)
type kvStore struct {
Codecs []securecookie.Codec
Options *sessions.Options
DefaultMaxAge int
prefix string
serializer SessionSerializer
store cache.Driver
}
func newKvStore(prefix string, store cache.Driver, keyPairs ...[]byte) *kvStore {
return &kvStore{
prefix: prefix,
store: store,
DefaultMaxAge: 60 * 20,
serializer: GobSerializer{},
Codecs: securecookie.CodecsFromPairs(keyPairs...),
Options: &sessions.Options{
Path: "/",
MaxAge: 86400 * 30,
},
}
}
// Get returns a session for the given name after adding it to the registry.
//
// It returns a new session if the sessions doesn't exist. Access IsNew on
// the session to check if it is an existing session or a new one.
//
// It returns a new session and an error if the session exists but could
// not be decoded.
func (s *kvStore) Get(r *http.Request, name string) (*sessions.Session, error) {
return sessions.GetRegistry(r).Get(s, name)
}
// New returns a session for the given name without adding it to the registry.
//
// The difference between New() and Get() is that calling New() twice will
// decode the session data twice, while Get() registers and reuses the same
// decoded session after the first call.
func (s *kvStore) New(r *http.Request, name string) (*sessions.Session, error) {
var (
err error
)
session := sessions.NewSession(s, name)
// make a copy
options := *s.Options
session.Options = &options
session.IsNew = true
if c, errCookie := r.Cookie(name); errCookie == nil {
err = securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...)
if err == nil {
res, ok := s.store.Get(s.prefix + session.ID)
if ok {
err = s.serializer.Deserialize(res.([]byte), session)
}
session.IsNew = !(err == nil && ok) // not new if no error and data available
}
}
return session, err
}
func (s *kvStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
// Marked for deletion.
if session.Options.MaxAge <= 0 {
if err := s.store.Delete([]string{session.ID}, s.prefix); err != nil {
return err
}
http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options))
} else {
// Build an alphanumeric key for the redis store.
if session.ID == "" {
session.ID = strings.TrimRight(base32.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(32)), "=")
}
b, err := s.serializer.Serialize(session)
if err != nil {
return err
}
age := session.Options.MaxAge
if age == 0 {
age = s.DefaultMaxAge
}
if err := s.store.Set(s.prefix+session.ID, b, age); err != nil {
return err
}
encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, s.Codecs...)
if err != nil {
return err
}
http.SetCookie(w, sessions.NewCookie(session.Name(), encoded, session.Options))
}
return nil
}
// SessionSerializer provides an interface hook for alternative serializers
type SessionSerializer interface {
Deserialize(d []byte, ss *sessions.Session) error
Serialize(ss *sessions.Session) ([]byte, error)
}
// GobSerializer uses gob package to encode the session map
type GobSerializer struct{}
// Serialize using gob
func (s GobSerializer) Serialize(ss *sessions.Session) ([]byte, error) {
buf := new(bytes.Buffer)
enc := gob.NewEncoder(buf)
err := enc.Encode(ss.Values)
if err == nil {
return buf.Bytes(), nil
}
return nil, err
}
// Deserialize back to map[interface{}]interface{}
func (s GobSerializer) Deserialize(d []byte, ss *sessions.Session) error {
dec := gob.NewDecoder(bytes.NewBuffer(d))
return dec.Decode(&ss.Values)
}
+22
View File
@@ -0,0 +1,22 @@
package sessionstore
import (
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/gin-contrib/sessions"
)
type Store interface {
sessions.Store
}
func NewStore(driver cache.Driver, keyPairs ...[]byte) Store {
return &store{newKvStore("cd_session_", driver, keyPairs...)}
}
type store struct {
*kvStore
}
func (c *store) Options(options sessions.Options) {
c.kvStore.Options = options.ToGorillaOptions()
}
+57 -19
View File
@@ -1,7 +1,7 @@
package thumb
import (
"errors"
"context"
"fmt"
"image"
"image/gif"
@@ -13,11 +13,15 @@ import (
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/gofrs/uuid"
//"github.com/nfnt/resize"
"golang.org/x/image/draw"
)
func init() {
RegisterGenerator(&Builtin{})
}
// Thumb 缩略图
type Thumb struct {
src image.Image
@@ -30,25 +34,23 @@ func NewThumbFromFile(file io.Reader, name string) (*Thumb, error) {
ext := strings.ToLower(filepath.Ext(name))
// 无扩展名时
if len(ext) == 0 {
return nil, errors.New("未知的图像类型")
return nil, fmt.Errorf("unknown image format: %w", ErrPassThrough)
}
var err error
var img image.Image
switch ext[1:] {
case "jpg":
img, err = jpeg.Decode(file)
case "jpeg":
case "jpg", "jpeg":
img, err = jpeg.Decode(file)
case "gif":
img, err = gif.Decode(file)
case "png":
img, err = png.Decode(file)
default:
return nil, errors.New("unknown image format")
return nil, fmt.Errorf("unknown image format: %w", ErrPassThrough)
}
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to parse image: %w (%w)", err, ErrPassThrough)
}
return &Thumb{
@@ -70,18 +72,12 @@ func (image *Thumb) GetSize() (int, int) {
}
// Save 保存图像到给定路径
func (image *Thumb) Save(path string) (err error) {
out, err := util.CreatNestedFile(path)
if err != nil {
return err
}
defer out.Close()
func (image *Thumb) Save(w io.Writer) (err error) {
switch model.GetSettingByNameWithDefault("thumb_encode_method", "jpg") {
case "png":
err = png.Encode(out, image.src)
err = png.Encode(w, image.src)
default:
err = jpeg.Encode(out, image.src, &jpeg.Options{Quality: model.GetIntSetting("thumb_encode_quality", 85)})
err = jpeg.Encode(w, image.src, &jpeg.Options{Quality: model.GetIntSetting("thumb_encode_quality", 85)})
}
return err
@@ -141,9 +137,15 @@ func (image *Thumb) CreateAvatar(uid uint) error {
// 生成头像缩略图
src := image.src
for k, size := range []int{s, m, l} {
//image.src = resize.Resize(uint(size), uint(size), src, resize.Lanczos3)
out, err := util.CreatNestedFile(filepath.Join(savePath, fmt.Sprintf("avatar_%d_%d.png", uid, k)))
if err != nil {
return err
}
defer out.Close()
image.src = Resize(uint(size), uint(size), src)
err := image.Save(filepath.Join(savePath, fmt.Sprintf("avatar_%d_%d.png", uid, k)))
err = image.Save(out)
if err != nil {
return err
}
@@ -152,3 +154,39 @@ func (image *Thumb) CreateAvatar(uid uint) error {
return nil
}
type Builtin struct{}
func (b Builtin) Generate(ctx context.Context, file io.Reader, src, name string, options map[string]string) (*Result, error) {
img, err := NewThumbFromFile(file, name)
if err != nil {
return nil, err
}
img.GetThumb(thumbSize(options))
tempPath := filepath.Join(
util.RelativePath(model.GetSettingByName("temp_path")),
"thumb",
fmt.Sprintf("thumb_%s", uuid.Must(uuid.NewV4()).String()),
)
thumbFile, err := util.CreatNestedFile(tempPath)
if err != nil {
return nil, fmt.Errorf("failed to create temp file: %w", err)
}
defer thumbFile.Close()
if err := img.Save(thumbFile); err != nil {
return nil, err
}
return &Result{Path: tempPath}, nil
}
func (b Builtin) Priority() int {
return 300
}
func (b Builtin) EnableFlag() string {
return "thumb_builtin_enabled"
}
+93
View File
@@ -0,0 +1,93 @@
package thumb
import (
"bytes"
"context"
"fmt"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/gofrs/uuid"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
)
func init() {
RegisterGenerator(&FfmpegGenerator{})
}
type FfmpegGenerator struct {
exts []string
lastRawExts string
}
func (f *FfmpegGenerator) Generate(ctx context.Context, file io.Reader, src, name string, options map[string]string) (*Result, error) {
ffmpegOpts := model.GetSettingByNames("thumb_ffmpeg_path", "thumb_ffmpeg_exts", "thumb_ffmpeg_seek", "thumb_encode_method", "temp_path")
if f.lastRawExts != ffmpegOpts["thumb_ffmpeg_exts"] {
f.exts = strings.Split(ffmpegOpts["thumb_ffmpeg_exts"], ",")
}
if !util.IsInExtensionList(f.exts, name) {
return nil, fmt.Errorf("unsupported video format: %w", ErrPassThrough)
}
tempOutputPath := filepath.Join(
util.RelativePath(ffmpegOpts["temp_path"]),
"thumb",
fmt.Sprintf("thumb_%s.%s", uuid.Must(uuid.NewV4()).String(), ffmpegOpts["thumb_encode_method"]),
)
tempInputPath := src
if tempInputPath == "" {
// If not local policy files, download to temp folder
tempInputPath = filepath.Join(
util.RelativePath(ffmpegOpts["temp_path"]),
"thumb",
fmt.Sprintf("ffmpeg_%s%s", uuid.Must(uuid.NewV4()).String(), filepath.Ext(name)),
)
// Due to limitations of ffmpeg, we need to write the input file to disk first
tempInputFile, err := util.CreatNestedFile(tempInputPath)
if err != nil {
return nil, fmt.Errorf("failed to create temp file: %w", err)
}
defer os.Remove(tempInputPath)
defer tempInputFile.Close()
if _, err = io.Copy(tempInputFile, file); err != nil {
return nil, fmt.Errorf("failed to write input file: %w", err)
}
tempInputFile.Close()
}
// Invoke ffmpeg
scaleOpt := fmt.Sprintf("scale=%s:%s:force_original_aspect_ratio=decrease", options["thumb_width"], options["thumb_height"])
cmd := exec.CommandContext(ctx,
ffmpegOpts["thumb_ffmpeg_path"], "-ss", ffmpegOpts["thumb_ffmpeg_seek"], "-i", tempInputPath,
"-vf", scaleOpt, "-vframes", "1", tempOutputPath)
// Redirect IO
var stdErr bytes.Buffer
cmd.Stdin = file
cmd.Stderr = &stdErr
if err := cmd.Run(); err != nil {
util.Log().Warning("Failed to invoke ffmpeg: %s", stdErr.String())
return nil, fmt.Errorf("failed to invoke ffmpeg: %w", err)
}
return &Result{Path: tempOutputPath}, nil
}
func (f *FfmpegGenerator) Priority() int {
return 200
}
func (f *FfmpegGenerator) EnableFlag() string {
return "thumb_ffmpeg_enabled"
}
-146
View File
@@ -1,146 +0,0 @@
package thumb
import (
"fmt"
"image"
"image/jpeg"
"os"
"testing"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/stretchr/testify/assert"
)
func CreateTestImage() *os.File {
file, err := os.Create("TestNewThumbFromFile.jpeg")
alpha := image.NewAlpha(image.Rect(0, 0, 500, 200))
jpeg.Encode(file, alpha, nil)
if err != nil {
fmt.Println(err)
}
_, _ = file.Seek(0, 0)
return file
}
func TestNewThumbFromFile(t *testing.T) {
asserts := assert.New(t)
file := CreateTestImage()
defer file.Close()
// 无扩展名时
{
thumb, err := NewThumbFromFile(file, "123")
asserts.Error(err)
asserts.Nil(thumb)
}
{
thumb, err := NewThumbFromFile(file, "123.jpg")
asserts.NoError(err)
asserts.NotNil(thumb)
}
{
thumb, err := NewThumbFromFile(file, "123.jpeg")
asserts.Error(err)
asserts.Nil(thumb)
}
{
thumb, err := NewThumbFromFile(file, "123.png")
asserts.Error(err)
asserts.Nil(thumb)
}
{
thumb, err := NewThumbFromFile(file, "123.gif")
asserts.Error(err)
asserts.Nil(thumb)
}
{
thumb, err := NewThumbFromFile(file, "123.3211")
asserts.Error(err)
asserts.Nil(thumb)
}
}
func TestThumb_GetSize(t *testing.T) {
asserts := assert.New(t)
file := CreateTestImage()
defer file.Close()
thumb, err := NewThumbFromFile(file, "123.jpg")
asserts.NoError(err)
w, h := thumb.GetSize()
asserts.Equal(500, w)
asserts.Equal(200, h)
}
func TestThumb_GetThumb(t *testing.T) {
asserts := assert.New(t)
file := CreateTestImage()
defer file.Close()
thumb, err := NewThumbFromFile(file, "123.jpg")
asserts.NoError(err)
asserts.NotPanics(func() {
thumb.GetThumb(10, 10)
})
}
func TestThumb_Thumbnail(t *testing.T) {
asserts := assert.New(t)
{
img := image.NewRGBA(image.Rect(0, 0, 500, 200))
thumb := Thumbnail(100, 100, img)
asserts.Equal(thumb.Bounds(), image.Rect(0, 0, 100, 40))
}
{
img := image.NewRGBA(image.Rect(0, 0, 200, 200))
thumb := Thumbnail(100, 100, img)
asserts.Equal(thumb.Bounds(), image.Rect(0, 0, 100, 100))
}
{
img := image.NewRGBA(image.Rect(0, 0, 500, 500))
thumb := Thumbnail(100, 100, img)
asserts.Equal(thumb.Bounds(), image.Rect(0, 0, 100, 100))
}
{
img := image.NewRGBA(image.Rect(0, 0, 200, 500))
thumb := Thumbnail(100, 100, img)
asserts.Equal(thumb.Bounds(), image.Rect(0, 0, 40, 100))
}
}
func TestThumb_Save(t *testing.T) {
asserts := assert.New(t)
file := CreateTestImage()
defer file.Close()
thumb, err := NewThumbFromFile(file, "123.jpg")
asserts.NoError(err)
err = thumb.Save("/:noteexist/")
asserts.Error(err)
err = thumb.Save("TestThumb_Save.png")
asserts.NoError(err)
asserts.True(util.Exists("TestThumb_Save.png"))
}
func TestThumb_CreateAvatar(t *testing.T) {
asserts := assert.New(t)
file := CreateTestImage()
defer file.Close()
thumb, err := NewThumbFromFile(file, "123.jpg")
asserts.NoError(err)
cache.Set("setting_avatar_path", "tests", 0)
cache.Set("setting_avatar_size_s", "50", 0)
cache.Set("setting_avatar_size_m", "130", 0)
cache.Set("setting_avatar_size_l", "200", 0)
asserts.NoError(thumb.CreateAvatar(1))
asserts.True(util.Exists(util.RelativePath("tests/avatar_1_1.png")))
asserts.True(util.Exists(util.RelativePath("tests/avatar_1_2.png")))
asserts.True(util.Exists(util.RelativePath("tests/avatar_1_0.png")))
}
+99
View File
@@ -0,0 +1,99 @@
package thumb
import (
"bytes"
"context"
"fmt"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/gofrs/uuid"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
)
func init() {
RegisterGenerator(&LibreOfficeGenerator{})
}
type LibreOfficeGenerator struct {
exts []string
lastRawExts string
}
func (l *LibreOfficeGenerator) Generate(ctx context.Context, file io.Reader, src string, name string, options map[string]string) (*Result, error) {
sofficeOpts := model.GetSettingByNames("thumb_libreoffice_path", "thumb_libreoffice_exts", "thumb_encode_method", "temp_path")
if l.lastRawExts != sofficeOpts["thumb_libreoffice_exts"] {
l.exts = strings.Split(sofficeOpts["thumb_libreoffice_exts"], ",")
}
if !util.IsInExtensionList(l.exts, name) {
return nil, fmt.Errorf("unsupported document format: %w", ErrPassThrough)
}
tempOutputPath := filepath.Join(
util.RelativePath(sofficeOpts["temp_path"]),
"thumb",
fmt.Sprintf("soffice_%s", uuid.Must(uuid.NewV4()).String()),
)
tempInputPath := src
if tempInputPath == "" {
// If not local policy files, download to temp folder
tempInputPath = filepath.Join(
util.RelativePath(sofficeOpts["temp_path"]),
"thumb",
fmt.Sprintf("soffice_%s%s", uuid.Must(uuid.NewV4()).String(), filepath.Ext(name)),
)
// Due to limitations of ffmpeg, we need to write the input file to disk first
tempInputFile, err := util.CreatNestedFile(tempInputPath)
if err != nil {
return nil, fmt.Errorf("failed to create temp file: %w", err)
}
defer os.Remove(tempInputPath)
defer tempInputFile.Close()
if _, err = io.Copy(tempInputFile, file); err != nil {
return nil, fmt.Errorf("failed to write input file: %w", err)
}
tempInputFile.Close()
}
// Convert the document to an image
cmd := exec.CommandContext(ctx, sofficeOpts["thumb_libreoffice_path"], "--headless",
"-nologo", "--nofirststartwizard", "--invisible", "--norestore", "--convert-to",
sofficeOpts["thumb_encode_method"], "--outdir", tempOutputPath, tempInputPath)
// Redirect IO
var stdErr bytes.Buffer
cmd.Stdin = file
cmd.Stderr = &stdErr
if err := cmd.Run(); err != nil {
util.Log().Warning("Failed to invoke LibreOffice: %s", stdErr.String())
return nil, fmt.Errorf("failed to invoke LibreOffice: %w", err)
}
return &Result{
Path: filepath.Join(
tempOutputPath,
strings.TrimSuffix(filepath.Base(tempInputPath), filepath.Ext(tempInputPath))+"."+sofficeOpts["thumb_encode_method"],
),
Continue: true,
Cleanup: []func(){func() { _ = os.RemoveAll(tempOutputPath) }},
}, nil
}
func (l *LibreOfficeGenerator) Priority() int {
return 50
}
func (l *LibreOfficeGenerator) EnableFlag() string {
return "thumb_libreoffice_enabled"
}
+122
View File
@@ -0,0 +1,122 @@
package thumb
import (
"context"
"errors"
"fmt"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"io"
"os"
"path/filepath"
"reflect"
"sort"
"strconv"
)
// Generator generates a thumbnail for a given reader.
type Generator interface {
// Generate generates a thumbnail for a given reader. Src is the original file path, only provided
// for local policy files.
Generate(ctx context.Context, file io.Reader, src string, name string, options map[string]string) (*Result, error)
// Priority of execution order, smaller value means higher priority.
Priority() int
// EnableFlag returns the setting name to enable this generator.
EnableFlag() string
}
type Result struct {
Path string
Continue bool
Cleanup []func()
}
type (
GeneratorType string
GeneratorList []Generator
)
var (
Generators = GeneratorList{}
ErrPassThrough = errors.New("pass through")
ErrNotAvailable = fmt.Errorf("thumbnail not available: %w", ErrPassThrough)
)
func (g GeneratorList) Len() int {
return len(g)
}
func (g GeneratorList) Less(i, j int) bool {
return g[i].Priority() < g[j].Priority()
}
func (g GeneratorList) Swap(i, j int) {
g[i], g[j] = g[j], g[i]
}
// RegisterGenerator registers a thumbnail generator.
func RegisterGenerator(generator Generator) {
Generators = append(Generators, generator)
sort.Sort(Generators)
}
func (p GeneratorList) Generate(ctx context.Context, file io.Reader, src, name string, options map[string]string) (*Result, error) {
inputFile, inputSrc, inputName := file, src, name
for _, generator := range p {
if model.IsTrueVal(options[generator.EnableFlag()]) {
res, err := generator.Generate(ctx, inputFile, inputSrc, inputName, options)
if errors.Is(err, ErrPassThrough) {
util.Log().Debug("Failed to generate thumbnail using %s for %s: %s, passing through to next generator.", reflect.TypeOf(generator).String(), name, err)
continue
}
if res != nil && res.Continue {
util.Log().Debug("Generator %s for %s returned continue, passing through to next generator.", reflect.TypeOf(generator).String(), name)
// defer cleanup funcs
for _, cleanup := range res.Cleanup {
defer cleanup()
}
// prepare file reader for next generator
intermediate, err := os.Open(res.Path)
if err != nil {
return nil, fmt.Errorf("failed to open intermediate thumb file: %w", err)
}
defer intermediate.Close()
inputFile = intermediate
inputSrc = res.Path
inputName = filepath.Base(res.Path)
continue
}
return res, err
}
}
return nil, ErrNotAvailable
}
func (p GeneratorList) Priority() int {
return 0
}
func (p GeneratorList) EnableFlag() string {
return ""
}
func thumbSize(options map[string]string) (uint, uint) {
w, h := uint(400), uint(300)
if wParsed, err := strconv.Atoi(options["thumb_width"]); err == nil {
w = uint(wParsed)
}
if hParsed, err := strconv.Atoi(options["thumb_height"]); err == nil {
h = uint(hParsed)
}
return w, h
}
+74
View File
@@ -0,0 +1,74 @@
package thumb
import (
"bytes"
"context"
"errors"
"fmt"
"os/exec"
"strings"
)
var (
ErrUnknownGenerator = errors.New("unknown generator type")
ErrUnknownOutput = errors.New("unknown output from generator")
)
// TestGenerator tests thumb generator by getting lib version
func TestGenerator(ctx context.Context, name, executable string) (string, error) {
switch name {
case "vips":
return testVipsGenerator(ctx, executable)
case "ffmpeg":
return testFfmpegGenerator(ctx, executable)
case "libreOffice":
return testLibreOfficeGenerator(ctx, executable)
default:
return "", ErrUnknownGenerator
}
}
func testVipsGenerator(ctx context.Context, executable string) (string, error) {
cmd := exec.CommandContext(ctx, executable, "--version")
var output bytes.Buffer
cmd.Stdout = &output
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("failed to invoke vips executable: %w", err)
}
if !strings.Contains(output.String(), "vips") {
return "", ErrUnknownOutput
}
return output.String(), nil
}
func testFfmpegGenerator(ctx context.Context, executable string) (string, error) {
cmd := exec.CommandContext(ctx, executable, "-version")
var output bytes.Buffer
cmd.Stdout = &output
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("failed to invoke ffmpeg executable: %w", err)
}
if !strings.Contains(output.String(), "ffmpeg") {
return "", ErrUnknownOutput
}
return output.String(), nil
}
func testLibreOfficeGenerator(ctx context.Context, executable string) (string, error) {
cmd := exec.CommandContext(ctx, executable, "--version")
var output bytes.Buffer
cmd.Stdout = &output
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("failed to invoke libreoffice executable: %w", err)
}
if !strings.Contains(output.String(), "LibreOffice") {
return "", ErrUnknownOutput
}
return output.String(), nil
}
+78
View File
@@ -0,0 +1,78 @@
package thumb
import (
"bytes"
"context"
"fmt"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"github.com/gofrs/uuid"
"io"
"os/exec"
"path/filepath"
"strings"
)
func init() {
RegisterGenerator(&VipsGenerator{})
}
type VipsGenerator struct {
exts []string
lastRawExts string
}
func (v *VipsGenerator) Generate(ctx context.Context, file io.Reader, src, name string, options map[string]string) (*Result, error) {
vipsOpts := model.GetSettingByNames("thumb_vips_path", "thumb_vips_exts", "thumb_encode_quality", "thumb_encode_method", "temp_path")
if v.lastRawExts != vipsOpts["thumb_vips_exts"] {
v.exts = strings.Split(vipsOpts["thumb_vips_exts"], ",")
}
if !util.IsInExtensionList(v.exts, name) {
return nil, fmt.Errorf("unsupported image format: %w", ErrPassThrough)
}
outputOpt := ".png"
if vipsOpts["thumb_encode_method"] == "jpg" {
outputOpt = fmt.Sprintf(".jpg[Q=%s]", vipsOpts["thumb_encode_quality"])
}
cmd := exec.CommandContext(ctx,
vipsOpts["thumb_vips_path"], "thumbnail_source", "[descriptor=0]", outputOpt, options["thumb_width"],
"--height", options["thumb_height"])
tempPath := filepath.Join(
util.RelativePath(vipsOpts["temp_path"]),
"thumb",
fmt.Sprintf("thumb_%s", uuid.Must(uuid.NewV4()).String()),
)
thumbFile, err := util.CreatNestedFile(tempPath)
if err != nil {
return nil, fmt.Errorf("failed to create temp file: %w", err)
}
defer thumbFile.Close()
// Redirect IO
var vipsErr bytes.Buffer
cmd.Stdin = file
cmd.Stdout = thumbFile
cmd.Stderr = &vipsErr
if err := cmd.Run(); err != nil {
util.Log().Warning("Failed to invoke vips: %s", vipsErr.String())
return nil, fmt.Errorf("failed to invoke vips: %w", err)
}
return &Result{Path: tempPath}, nil
}
func (v *VipsGenerator) Priority() int {
return 100
}
func (v *VipsGenerator) EnableFlag() string {
return "thumb_vips_enabled"
}

Some files were not shown because too many files have changed in this diff Show More