mirror of
https://github.com/cloudreve/cloudreve.git
synced 2026-03-11 02:27:01 +00:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
213eaa54dd | ||
|
|
e7d6fb25e4 | ||
|
|
e3e08a9b75 | ||
|
|
78f7ec8b08 | ||
|
|
3d41e00384 | ||
|
|
5e5dca40c4 | ||
|
|
668b542c59 | ||
|
|
440ab775b8 | ||
|
|
678593f30d | ||
|
|
58ceae9708 | ||
|
|
3b8110b648 | ||
|
|
f0c5b08428 | ||
|
|
9434c2f29b | ||
|
|
7d97237593 | ||
|
|
a581851f84 | ||
|
|
fe7cf5d0d8 | ||
|
|
cec2b55e1e | ||
|
|
af43746ba2 | ||
|
|
9f1cb52cfb | ||
|
|
4acf9401b8 | ||
|
|
c3ed4f5839 | ||
|
|
9b40e0146f | ||
|
|
a16b491f65 | ||
|
|
a095117061 | ||
|
|
acc660f112 | ||
|
|
a677e23394 | ||
|
|
13e774f27d | ||
|
|
91717b7c49 | ||
|
|
a1ce16bd5e | ||
|
|
872b08e5da | ||
|
|
f73583b370 | ||
|
|
c0132a10cb | ||
|
|
927c3bff00 | ||
|
|
bb9b42eb10 | ||
|
|
5f18d277c8 | ||
|
|
b0057fe92f | ||
|
|
bb3db2e326 | ||
|
|
8deeadb1e5 | ||
|
|
8688069fac | ||
|
|
4c08644b05 |
@@ -3,7 +3,7 @@ package constants
|
||||
// These values will be injected at build time, DO NOT EDIT.
|
||||
|
||||
// BackendVersion 当前后端版本号
|
||||
var BackendVersion = "4.1.0"
|
||||
var BackendVersion = "4.7.0"
|
||||
|
||||
// IsPro 是否为Pro版本
|
||||
var IsPro = "false"
|
||||
|
||||
@@ -467,7 +467,7 @@ func (d *dependency) MediaMetaExtractor(ctx context.Context) mediameta.Extractor
|
||||
return d.mediaMeta
|
||||
}
|
||||
|
||||
d.mediaMeta = mediameta.NewExtractorManager(ctx, d.SettingProvider(), d.Logger())
|
||||
d.mediaMeta = mediameta.NewExtractorManager(ctx, d.SettingProvider(), d.Logger(), d.RequestClient())
|
||||
return d.mediaMeta
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ type system struct {
|
||||
Debug bool
|
||||
SessionSecret string
|
||||
HashIDSalt string
|
||||
GracePeriod int `validate:"gte=0"`
|
||||
ProxyHeader string `validate:"required_with=Listen"`
|
||||
GracePeriod int `validate:"gte=0"`
|
||||
ProxyHeader string
|
||||
}
|
||||
|
||||
type ssl struct {
|
||||
|
||||
@@ -22,7 +22,7 @@ var SystemConfig = &system{
|
||||
Debug: false,
|
||||
Mode: "master",
|
||||
Listen: ":5212",
|
||||
ProxyHeader: "X-Forwarded-For",
|
||||
ProxyHeader: "",
|
||||
}
|
||||
|
||||
// CORSConfig 跨域配置
|
||||
|
||||
@@ -103,10 +103,6 @@ func (m *Migrator) migratePolicy() (map[int]bool, error) {
|
||||
settings.ProxyServer = policy.OptionsSerialized.OdProxy
|
||||
}
|
||||
|
||||
if policy.DirNameRule == "" {
|
||||
policy.DirNameRule = "uploads/{uid}/{path}"
|
||||
}
|
||||
|
||||
if policy.Type == types.PolicyTypeCos {
|
||||
settings.ChunkSize = 1024 * 1024 * 25
|
||||
}
|
||||
@@ -122,8 +118,16 @@ func (m *Migrator) migratePolicy() (map[int]bool, error) {
|
||||
hasRandomElement = true
|
||||
break
|
||||
}
|
||||
|
||||
if strings.Contains(policy.DirNameRule, c) {
|
||||
hasRandomElement = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasRandomElement {
|
||||
if policy.DirNameRule == "" {
|
||||
policy.DirNameRule = "uploads/{uid}/{path}"
|
||||
}
|
||||
policy.FileNameRule = "{uid}_{randomkey8}_{originname}"
|
||||
m.l.Warning("Storage policy %q has no random element in file name rule, using default file name rule.", policy.Name)
|
||||
}
|
||||
|
||||
2
assets
2
assets
Submodule assets updated: 3a23464a0f...71e5fbd240
@@ -5,9 +5,11 @@ services:
|
||||
depends_on:
|
||||
- postgresql
|
||||
- redis
|
||||
restart: always
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 5212:5212
|
||||
- 6888:6888
|
||||
- 6888:6888/udp
|
||||
environment:
|
||||
- CR_CONF_Database.Type=postgres
|
||||
- CR_CONF_Database.Host=postgresql
|
||||
@@ -24,6 +26,7 @@ services:
|
||||
# backup & consult https://www.postgresql.org/docs/current/pgupgrade.html
|
||||
image: postgres:17
|
||||
container_name: postgresql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=cloudreve
|
||||
- POSTGRES_DB=cloudreve
|
||||
@@ -34,6 +37,7 @@ services:
|
||||
redis:
|
||||
image: redis:latest
|
||||
container_name: redis
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
|
||||
|
||||
35
go.mod
35
go.mod
@@ -6,8 +6,9 @@ require (
|
||||
entgo.io/ent v0.13.0
|
||||
github.com/Masterminds/semver/v3 v3.3.1
|
||||
github.com/abslant/gzip v0.0.9
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
||||
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.3.0
|
||||
github.com/aws/aws-sdk-go v1.31.5
|
||||
github.com/bodgit/sevenzip v1.6.0
|
||||
github.com/cloudflare/cfssl v1.6.1
|
||||
github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25
|
||||
github.com/dsoprea/go-exif/v3 v3.0.1
|
||||
@@ -22,7 +23,6 @@ require (
|
||||
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/go-ini/ini v1.50.0
|
||||
github.com/go-mail/mail v2.3.1+incompatible
|
||||
github.com/go-playground/validator/v10 v10.20.0
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/go-webauthn/webauthn v0.11.2
|
||||
@@ -40,7 +40,7 @@ require (
|
||||
github.com/juju/ratelimit v1.0.1
|
||||
github.com/ks3sdklib/aws-sdk-go v1.6.2
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/mholt/archiver/v4 v4.0.0-alpha.6
|
||||
github.com/mholt/archives v0.1.3
|
||||
github.com/mojocn/base64Captcha v0.0.0-20190801020520-752b1cd608b2
|
||||
github.com/pquerna/otp v1.2.0
|
||||
github.com/qiniu/go-sdk/v7 v7.19.0
|
||||
@@ -54,6 +54,7 @@ require (
|
||||
github.com/tencentyun/cos-go-sdk-v5 v0.7.54
|
||||
github.com/ua-parser/uap-go v0.0.0-20250213224047-9c035f085b90
|
||||
github.com/upyun/go-sdk v2.1.0+incompatible
|
||||
github.com/wneessen/go-mail v0.6.2
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410
|
||||
golang.org/x/text v0.23.0
|
||||
@@ -65,9 +66,12 @@ require (
|
||||
require (
|
||||
ariga.io/atlas v0.19.1-0.20240203083654-5948b60a8e43 // indirect
|
||||
cloud.google.com/go v0.81.0 // indirect
|
||||
github.com/STARRY-S/zip v0.2.1 // indirect
|
||||
github.com/agext/levenshtein v1.2.1 // indirect
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 // indirect
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
|
||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||
github.com/bodgit/windows v1.0.1 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
@@ -76,7 +80,7 @@ require (
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
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/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/dsoprea/go-exif/v2 v2.0.0-20200604193436-ca8584a0e1c4 // indirect
|
||||
github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb // indirect
|
||||
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
|
||||
@@ -95,10 +99,11 @@ require (
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/go-tpm v0.9.1 // indirect
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.13.0 // indirect
|
||||
@@ -106,32 +111,34 @@ require (
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.3.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.7 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/klauspost/pgzip v1.2.5 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||
github.com/minio/minlz v1.0.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mozillazg/go-httpheader v0.4.0 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.14 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.5 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/therootcompany/xz v1.0.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/ulikunitz/xz v0.5.10 // indirect
|
||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/zclconf/go-cty v1.8.0 // indirect
|
||||
go4.org v0.0.0-20200411211856-f5505b9728dd // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
@@ -139,8 +146,6 @@ require (
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
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/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||
|
||||
112
go.sum
112
go.sum
@@ -82,6 +82,8 @@ github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuN
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM=
|
||||
github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg=
|
||||
github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
||||
@@ -98,10 +100,10 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.3.0 h1:wQlqotpyjYPjJz+Noh5bRu7Snmydk8SKC5Z6u1CR20Y=
|
||||
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.3.0/go.mod h1:FTzydeQVmR24FI0D6XWUOMKckjXehM/jgMn1xC+DA9M=
|
||||
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 h1:8PmGpDEZl9yDpcdEr6Odf23feCxK3LNUNMxjXg41pZQ=
|
||||
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
|
||||
@@ -138,6 +140,12 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
|
||||
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/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
|
||||
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
|
||||
github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A=
|
||||
github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc=
|
||||
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
|
||||
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
|
||||
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/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
@@ -213,8 +221,8 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8
|
||||
github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25 h1:simG0vMYFvNriGhaaat7QVVkaVkXzvqcohaBoLZl9Hg=
|
||||
github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25/go.mod h1:Z3Lomva4pyMWYezjMAU5QWRh0p1VvO4199OHlFnyKkM=
|
||||
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
|
||||
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
|
||||
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
|
||||
github.com/dsoprea/go-exif/v2 v2.0.0-20200520183328-015129a9efd5/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0=
|
||||
@@ -325,8 +333,6 @@ github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgO
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-mail/mail v2.3.1+incompatible h1:UzNOn0k5lpfVtO31cK3hn6I4VEVGhe3lX8AJBAxXExM=
|
||||
github.com/go-mail/mail v2.3.1+incompatible/go.mod h1:VPWjmmNyRsWXQZHVHT3g0YbIINUkSmuKOiLIDkWbL6M=
|
||||
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
|
||||
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
@@ -416,8 +422,6 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
|
||||
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
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 v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s=
|
||||
github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
@@ -521,11 +525,15 @@ github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoP
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
@@ -612,14 +620,14 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
||||
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||
github.com/kisom/goutils v1.4.3/go.mod h1:Lp5qrquG7yhYnWzZCI/68Pa/GpFynw//od6EkGnWpac=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
|
||||
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
|
||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@@ -688,11 +696,15 @@ github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S
|
||||
github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/mholt/archiver/v4 v4.0.0-alpha.6 h1:3wvos9Kn1GpKNBz+MpozinGREPslLo1ds1W16vTkErQ=
|
||||
github.com/mholt/archiver/v4 v4.0.0-alpha.6/go.mod h1:9PTygYq90FQBWPspdwAng6dNjYiBuTYKqmA6c15KuCo=
|
||||
github.com/mholt/archives v0.1.3 h1:aEAaOtNra78G+TvV5ohmXrJOAzf++dIlYeDW3N9q458=
|
||||
github.com/mholt/archives v0.1.3/go.mod h1:LUCGp++/IbV/I0Xq4SzcIR6uwgeh2yjnQWamjRQfLTU=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
|
||||
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
|
||||
github.com/minio/minlz v1.0.0 h1:Kj7aJZ1//LlTP1DM8Jm7lNKvvJS2m74gyyXXn3+uJWQ=
|
||||
github.com/minio/minlz v1.0.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
@@ -738,8 +750,8 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/nishanths/predeclared v0.0.0-20200524104333-86fad755b4d3/go.mod h1:nt3d53pc1VYcphSCIaYAJtnPYnr3Zyn8fMq2wvPGPso=
|
||||
github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc=
|
||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk=
|
||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=
|
||||
github.com/nwaples/rardecode/v2 v2.1.0 h1:JQl9ZoBPDy+nIZGb1mx8+anfHp/LV3NE2MjMiv0ct/U=
|
||||
github.com/nwaples/rardecode/v2 v2.1.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
||||
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
@@ -776,8 +788,8 @@ github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h
|
||||
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
|
||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE=
|
||||
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -879,6 +891,8 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k
|
||||
github.com/soheilhy/cmux v0.1.5-0.20210205191134-5ec6847320e5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
|
||||
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
|
||||
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg=
|
||||
github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/speps/go-hashids v2.0.0+incompatible h1:kSfxGfESueJKTx0mpER9Y/1XHl+FVQjtCqRyYcviFbw=
|
||||
github.com/speps/go-hashids v2.0.0+incompatible/go.mod h1:P7hqPzMdnZOfyIk+xrlG1QaSMw+gCBdHKsBDnhpaZvc=
|
||||
@@ -930,8 +944,6 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0=
|
||||
github.com/tencentyun/cos-go-sdk-v5 v0.7.54 h1:FRamEhNBbSeggyYfWfzFejTLftgbICocSYFk4PKTSV4=
|
||||
github.com/tencentyun/cos-go-sdk-v5 v0.7.54/go.mod h1:UN+VdbCl1hg+kKi5RXqZgaP+Boqfmk+D04GRc4XFk70=
|
||||
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
|
||||
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
|
||||
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
|
||||
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
|
||||
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
|
||||
@@ -953,8 +965,9 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
|
||||
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/upyun/go-sdk v2.1.0+incompatible h1:OdjXghQ/TVetWV16Pz3C1/SUpjhGBVPr+cLiqZLLyq0=
|
||||
github.com/upyun/go-sdk v2.1.0+incompatible/go.mod h1:eu3F5Uz4b9ZE5bE5QsCL6mgSNWRwfj0zpJ9J626HEqs=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
@@ -967,6 +980,8 @@ github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+
|
||||
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
|
||||
github.com/weppos/publicsuffix-go v0.13.1-0.20210123135404-5fd73613514e/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE=
|
||||
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE=
|
||||
github.com/wneessen/go-mail v0.6.2 h1:c6V7c8D2mz868z9WJ+8zDKtUyLfZ1++uAZmo2GRFji8=
|
||||
github.com/wneessen/go-mail v0.6.2/go.mod h1:L/PYjPK3/2ZlNb2/FjEBIn9n1rUWjW+Toy531oVmeb4=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug=
|
||||
@@ -974,6 +989,8 @@ github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0B
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@@ -1023,8 +1040,9 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
|
||||
go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU=
|
||||
go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
|
||||
gocloud.dev v0.19.0/go.mod h1:SmKwiR8YwIMMJvQBKLsC3fHNyMwXLw3PMDO+VVteJMI=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
@@ -1051,6 +1069,10 @@ golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -1095,6 +1117,10 @@ 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/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1151,6 +1177,12 @@ golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -1181,6 +1213,11 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1257,12 +1294,24 @@ golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
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/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1273,6 +1322,12 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -1348,6 +1403,9 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -1484,8 +1542,6 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -1501,8 +1557,6 @@ gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/R
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
|
||||
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
|
||||
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
|
||||
|
||||
@@ -57,6 +57,9 @@ type (
|
||||
UserID int
|
||||
Name string
|
||||
StoragePolicyID int
|
||||
HasMetadata string
|
||||
Shared bool
|
||||
HasDirectLink bool
|
||||
}
|
||||
|
||||
MetadataFilter struct {
|
||||
@@ -907,7 +910,7 @@ func (f *fileClient) CreateEntity(ctx context.Context, file *ent.File, args *Ent
|
||||
|
||||
diff := map[int]int64{file.OwnerID: created.Size}
|
||||
|
||||
if err := f.client.File.UpdateOne(file).AddEntities(created).Exec(ctx); err != nil {
|
||||
if err := f.client.Entity.UpdateOne(created).AddFile(file).Exec(ctx); err != nil {
|
||||
return nil, diff, fmt.Errorf("failed to add file entity: %v", err)
|
||||
}
|
||||
|
||||
@@ -1098,6 +1101,18 @@ func (f *fileClient) FlattenListFiles(ctx context.Context, args *FlattenListFile
|
||||
query = query.Where(file.NameContainsFold(args.Name))
|
||||
}
|
||||
|
||||
if args.HasMetadata != "" {
|
||||
query = query.Where(file.HasMetadataWith(metadata.Name(args.HasMetadata)))
|
||||
}
|
||||
|
||||
if args.Shared {
|
||||
query = query.Where(file.HasSharesWith(share.DeletedAtIsNil()))
|
||||
}
|
||||
|
||||
if args.HasDirectLink {
|
||||
query = query.Where(file.HasDirectLinksWith(directlink.DeletedAtIsNil()))
|
||||
}
|
||||
|
||||
query.Order(getFileOrderOption(&ListFileParameters{
|
||||
PaginationArgs: args.PaginationArgs,
|
||||
})...)
|
||||
|
||||
@@ -279,6 +279,53 @@ type (
|
||||
)
|
||||
|
||||
var patches = []Patch{
|
||||
{
|
||||
Name: "apply_default_archive_viewer",
|
||||
EndVersion: "4.7.0",
|
||||
Func: func(l logging.Logger, client *ent.Client, ctx context.Context) error {
|
||||
fileViewersSetting, err := client.Setting.Query().Where(setting.Name("file_viewers")).First(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to query file_viewers setting: %w", err)
|
||||
}
|
||||
|
||||
var fileViewers []types.ViewerGroup
|
||||
if err := json.Unmarshal([]byte(fileViewersSetting.Value), &fileViewers); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal file_viewers setting: %w", err)
|
||||
}
|
||||
|
||||
fileViewerExisted := false
|
||||
for _, viewer := range fileViewers[0].Viewers {
|
||||
if viewer.ID == "archive" {
|
||||
fileViewerExisted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 2.2 If not existed, add it
|
||||
if !fileViewerExisted {
|
||||
// Found existing archive viewer default setting
|
||||
var defaultArchiveViewer types.Viewer
|
||||
for _, viewer := range defaultFileViewers[0].Viewers {
|
||||
if viewer.ID == "archive" {
|
||||
defaultArchiveViewer = viewer
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fileViewers[0].Viewers = append(fileViewers[0].Viewers, defaultArchiveViewer)
|
||||
newFileViewersSetting, err := json.Marshal(fileViewers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal file_viewers setting: %w", err)
|
||||
}
|
||||
|
||||
if _, err := client.Setting.UpdateOne(fileViewersSetting).SetValue(string(newFileViewersSetting)).Save(ctx); err != nil {
|
||||
return fmt.Errorf("failed to update file_viewers setting: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "apply_default_excalidraw_viewer",
|
||||
EndVersion: "4.1.0",
|
||||
@@ -367,6 +414,69 @@ var patches = []Patch{
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "apply_email_title_magic_var",
|
||||
EndVersion: "4.7.0",
|
||||
Func: func(l logging.Logger, client *ent.Client, ctx context.Context) error {
|
||||
// 1. Activate Template
|
||||
mailActivationTemplateSetting, err := client.Setting.Query().Where(setting.Name("mail_activation_template")).First(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to query mail_activation_template setting: %w", err)
|
||||
}
|
||||
|
||||
var mailActivationTemplate []struct {
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Language string `json:"language"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(mailActivationTemplateSetting.Value), &mailActivationTemplate); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal mail_activation_template setting: %w", err)
|
||||
}
|
||||
|
||||
for i, t := range mailActivationTemplate {
|
||||
mailActivationTemplate[i].Title = fmt.Sprintf("[{{ .CommonContext.SiteBasic.Name }}] %s", t.Title)
|
||||
}
|
||||
|
||||
newMailActivationTemplate, err := json.Marshal(mailActivationTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal mail_activation_template setting: %w", err)
|
||||
}
|
||||
|
||||
if _, err := client.Setting.UpdateOne(mailActivationTemplateSetting).SetValue(string(newMailActivationTemplate)).Save(ctx); err != nil {
|
||||
return fmt.Errorf("failed to update mail_activation_template setting: %w", err)
|
||||
}
|
||||
|
||||
// 2. Reset Password Template
|
||||
mailResetTemplateSetting, err := client.Setting.Query().Where(setting.Name("mail_reset_template")).First(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to query mail_reset_template setting: %w", err)
|
||||
}
|
||||
|
||||
var mailResetTemplate []struct {
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Language string `json:"language"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(mailResetTemplateSetting.Value), &mailResetTemplate); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal mail_reset_template setting: %w", err)
|
||||
}
|
||||
|
||||
for i, t := range mailResetTemplate {
|
||||
mailResetTemplate[i].Title = fmt.Sprintf("[{{ .CommonContext.SiteBasic.Name }}] %s", t.Title)
|
||||
}
|
||||
|
||||
newMailResetTemplate, err := json.Marshal(mailResetTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal mail_reset_template setting: %w", err)
|
||||
}
|
||||
|
||||
if _, err := client.Setting.UpdateOne(mailResetTemplateSetting).SetValue(string(newMailResetTemplate)).Save(ctx); err != nil {
|
||||
return fmt.Errorf("failed to update mail_reset_template setting: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -7,17 +7,20 @@ import (
|
||||
// UserSetting 用户其他配置
|
||||
type (
|
||||
UserSetting struct {
|
||||
ProfileOff bool `json:"profile_off,omitempty"`
|
||||
PreferredTheme string `json:"preferred_theme,omitempty"`
|
||||
VersionRetention bool `json:"version_retention,omitempty"`
|
||||
VersionRetentionExt []string `json:"version_retention_ext,omitempty"`
|
||||
VersionRetentionMax int `json:"version_retention_max,omitempty"`
|
||||
Pined []PinedFile `json:"pined,omitempty"`
|
||||
Language string `json:"email_language,omitempty"`
|
||||
DisableViewSync bool `json:"disable_view_sync,omitempty"`
|
||||
FsViewMap map[string]ExplorerView `json:"fs_view_map,omitempty"`
|
||||
ProfileOff bool `json:"profile_off,omitempty"`
|
||||
PreferredTheme string `json:"preferred_theme,omitempty"`
|
||||
VersionRetention bool `json:"version_retention,omitempty"`
|
||||
VersionRetentionExt []string `json:"version_retention_ext,omitempty"`
|
||||
VersionRetentionMax int `json:"version_retention_max,omitempty"`
|
||||
Pined []PinedFile `json:"pined,omitempty"`
|
||||
Language string `json:"email_language,omitempty"`
|
||||
DisableViewSync bool `json:"disable_view_sync,omitempty"`
|
||||
FsViewMap map[string]ExplorerView `json:"fs_view_map,omitempty"`
|
||||
ShareLinksInProfile ShareLinksInProfileLevel `json:"share_links_in_profile,omitempty"`
|
||||
}
|
||||
|
||||
ShareLinksInProfileLevel string
|
||||
|
||||
PinedFile struct {
|
||||
Uri string `json:"uri"`
|
||||
Name string `json:"name,omitempty"`
|
||||
@@ -98,6 +101,8 @@ type (
|
||||
SourceAuth bool `json:"source_auth,omitempty"`
|
||||
// QiniuUploadCdn whether to use CDN for Qiniu upload.
|
||||
QiniuUploadCdn bool `json:"qiniu_upload_cdn,omitempty"`
|
||||
// ChunkConcurrency the number of chunks to upload concurrently.
|
||||
ChunkConcurrency int `json:"chunk_concurrency,omitempty"`
|
||||
}
|
||||
|
||||
FileType int
|
||||
@@ -252,6 +257,7 @@ func FileTypeFromString(s string) FileType {
|
||||
const (
|
||||
DavAccountReadOnly DavAccountOption = iota
|
||||
DavAccountProxy
|
||||
DavAccountDisableSysFiles
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -288,18 +294,19 @@ const (
|
||||
|
||||
type (
|
||||
Viewer struct {
|
||||
ID string `json:"id"`
|
||||
Type ViewerType `json:"type"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Exts []string `json:"exts"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
WopiActions map[string]map[ViewerAction]string `json:"wopi_actions,omitempty"`
|
||||
Props map[string]string `json:"props,omitempty"`
|
||||
MaxSize int64 `json:"max_size,omitempty"`
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
Templates []NewFileTemplate `json:"templates,omitempty"`
|
||||
Platform string `json:"platform,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Type ViewerType `json:"type"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Exts []string `json:"exts"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
WopiActions map[string]map[ViewerAction]string `json:"wopi_actions,omitempty"`
|
||||
Props map[string]string `json:"props,omitempty"`
|
||||
MaxSize int64 `json:"max_size,omitempty"`
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
Templates []NewFileTemplate `json:"templates,omitempty"`
|
||||
Platform string `json:"platform,omitempty"`
|
||||
RequiredGroupPermission []GroupPermission `json:"required_group_permission,omitempty"`
|
||||
}
|
||||
ViewerGroup struct {
|
||||
Viewers []Viewer `json:"viewers"`
|
||||
@@ -334,3 +341,9 @@ const (
|
||||
CustomPropsTypeLink = "link"
|
||||
CustomPropsTypeRating = "rating"
|
||||
)
|
||||
|
||||
const (
|
||||
ProfilePublicShareOnly = ShareLinksInProfileLevel("")
|
||||
ProfileAllShare = ShareLinksInProfileLevel("all_share")
|
||||
ProfileHideShare = ShareLinksInProfileLevel("hide_share")
|
||||
)
|
||||
|
||||
@@ -220,8 +220,29 @@ func (c *userClient) Delete(ctx context.Context, uid int) error {
|
||||
func (c *userClient) ApplyStorageDiff(ctx context.Context, diffs StorageDiff) error {
|
||||
ae := serializer.NewAggregateError()
|
||||
for uid, diff := range diffs {
|
||||
if err := c.client.User.Update().Where(user.ID(uid)).AddStorage(diff).Exec(ctx); err != nil {
|
||||
ae.Add(fmt.Sprintf("%d", uid), fmt.Errorf("failed to apply storage diff for user %d: %w", uid, err))
|
||||
// Retry logic for MySQL deadlock (Error 1213)
|
||||
// This is a temporary workaround. TODO: optimize storage mutation
|
||||
maxRetries := 3
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
if err := c.client.User.Update().Where(user.ID(uid)).AddStorage(diff).Exec(ctx); err != nil {
|
||||
lastErr = err
|
||||
// Check if it's a MySQL deadlock error (Error 1213)
|
||||
if strings.Contains(err.Error(), "Error 1213") && attempt < maxRetries-1 {
|
||||
// Wait a bit before retrying with exponential backoff
|
||||
time.Sleep(time.Duration(attempt+1) * 10 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
ae.Add(fmt.Sprintf("%d", uid), fmt.Errorf("failed to apply storage diff for user %d: %w", uid, err))
|
||||
break
|
||||
}
|
||||
// Success, break out of retry loop
|
||||
lastErr = nil
|
||||
break
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
ae.Add(fmt.Sprintf("%d", uid), fmt.Errorf("failed to apply storage diff for user %d: %w", uid, lastErr))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@ package middleware
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/application/constants"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/auth/requestinfo"
|
||||
@@ -14,8 +18,6 @@ import (
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gofrs/uuid"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HashID 将给定对象的HashID转换为真实ID
|
||||
@@ -92,8 +94,13 @@ func MobileRequestOnly() gin.HandlerFunc {
|
||||
// 2. Generate and inject correlation ID for diagnostic.
|
||||
func InitializeHandling(dep dependency.Dep) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
clientIp := c.ClientIP()
|
||||
if idx := strings.Index(clientIp, ","); idx > 0 {
|
||||
clientIp = clientIp[:idx]
|
||||
}
|
||||
|
||||
reqInfo := &requestinfo.RequestInfo{
|
||||
IP: c.ClientIP(),
|
||||
IP: clientIp,
|
||||
Host: c.Request.Host,
|
||||
UserAgent: c.Request.UserAgent(),
|
||||
}
|
||||
|
||||
@@ -180,9 +180,9 @@ func SlaveFileContentUrl(base *url.URL, srcPath, name string, download bool, spe
|
||||
return base
|
||||
}
|
||||
|
||||
func SlaveMediaMetaRoute(src, ext string) string {
|
||||
func SlaveMediaMetaRoute(src, ext, language string) string {
|
||||
src = url.PathEscape(base64.URLEncoding.EncodeToString([]byte(src)))
|
||||
return fmt.Sprintf("file/meta/%s/%s", src, url.PathEscape(ext))
|
||||
return fmt.Sprintf("file/meta/%s/%s?language=%s", src, url.PathEscape(ext), language)
|
||||
}
|
||||
|
||||
func SlaveFileListRoute(srcPath string, recursive bool) string {
|
||||
|
||||
@@ -46,7 +46,7 @@ type System struct {
|
||||
SessionSecret string
|
||||
HashIDSalt string // deprecated
|
||||
GracePeriod int `validate:"gte=0"`
|
||||
ProxyHeader string `validate:"required_with=Listen"`
|
||||
ProxyHeader string
|
||||
LogLevel string `validate:"oneof=debug info warning error"`
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ var SystemConfig = &System{
|
||||
Debug: false,
|
||||
Mode: MasterMode,
|
||||
Listen: ":5212",
|
||||
ProxyHeader: "X-Forwarded-For",
|
||||
ProxyHeader: "",
|
||||
LogLevel: "info",
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -9,8 +10,7 @@ import (
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
"github.com/go-mail/mail"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/wneessen/go-mail"
|
||||
)
|
||||
|
||||
// SMTPPool SMTP协议发送邮件
|
||||
@@ -38,9 +38,11 @@ type SMTPConfig struct {
|
||||
}
|
||||
|
||||
type message struct {
|
||||
msg *mail.Message
|
||||
cid string
|
||||
userID int
|
||||
msg *mail.Msg
|
||||
to string
|
||||
subject string
|
||||
cid string
|
||||
userID int
|
||||
}
|
||||
|
||||
// NewSMTPPool initializes a new SMTP based email sending queue.
|
||||
@@ -81,17 +83,21 @@ func (client *SMTPPool) Send(ctx context.Context, to, title, body string) error
|
||||
return nil
|
||||
}
|
||||
|
||||
m := mail.NewMessage()
|
||||
m.SetAddressHeader("From", client.config.From, client.config.FromName)
|
||||
m.SetAddressHeader("Reply-To", client.config.ReplyTo, client.config.FromName)
|
||||
m.SetHeader("To", to)
|
||||
m.SetHeader("Subject", title)
|
||||
m.SetHeader("Message-ID", fmt.Sprintf("<%s@%s>", uuid.Must(uuid.NewV4()).String(), "cloudreve"))
|
||||
m.SetBody("text/html", body)
|
||||
m := mail.NewMsg()
|
||||
if err := m.FromFormat(client.config.FromName, client.config.From); err != nil {
|
||||
return err
|
||||
}
|
||||
m.ReplyToFormat(client.config.FromName, client.config.ReplyTo)
|
||||
m.To(to)
|
||||
m.Subject(title)
|
||||
m.SetMessageID()
|
||||
m.SetBodyString(mail.TypeTextHTML, body)
|
||||
client.ch <- &message{
|
||||
msg: m,
|
||||
cid: logging.CorrelationID(ctx).String(),
|
||||
userID: inventory.UserIDFromContext(ctx),
|
||||
msg: m,
|
||||
subject: title,
|
||||
to: to,
|
||||
cid: logging.CorrelationID(ctx).String(),
|
||||
userID: inventory.UserIDFromContext(ctx),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -116,17 +122,24 @@ func (client *SMTPPool) Init() {
|
||||
}
|
||||
}()
|
||||
|
||||
d := mail.NewDialer(client.config.Host, client.config.Port, client.config.User, client.config.Password)
|
||||
d.Timeout = time.Duration(client.config.Keepalive+5) * time.Second
|
||||
client.chOpen = true
|
||||
// 是否启用 SSL
|
||||
d.SSL = false
|
||||
if client.config.ForceEncryption {
|
||||
d.SSL = true
|
||||
opts := []mail.Option{
|
||||
mail.WithPort(client.config.Port),
|
||||
mail.WithTimeout(time.Duration(client.config.Keepalive+5) * time.Second),
|
||||
mail.WithSMTPAuth(mail.SMTPAuthAutoDiscover), mail.WithTLSPortPolicy(mail.TLSOpportunistic),
|
||||
mail.WithUsername(client.config.User), mail.WithPassword(client.config.Password),
|
||||
}
|
||||
if client.config.ForceEncryption {
|
||||
opts = append(opts, mail.WithSSL())
|
||||
}
|
||||
d.StartTLSPolicy = mail.OpportunisticStartTLS
|
||||
|
||||
var s mail.SendCloser
|
||||
d, diaErr := mail.NewClient(client.config.Host, opts...)
|
||||
if diaErr != nil {
|
||||
client.l.Panic("Failed to create SMTP client: %s", diaErr)
|
||||
return
|
||||
}
|
||||
|
||||
client.chOpen = true
|
||||
|
||||
var err error
|
||||
open := false
|
||||
for {
|
||||
@@ -139,22 +152,32 @@ func (client *SMTPPool) Init() {
|
||||
}
|
||||
|
||||
if !open {
|
||||
if s, err = d.Dial(); err != nil {
|
||||
if err = d.DialWithContext(context.Background()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
open = true
|
||||
}
|
||||
|
||||
l := client.l.CopyWithPrefix(fmt.Sprintf("[Cid: %s]", m.cid))
|
||||
if err := mail.Send(s, m.msg); err != nil {
|
||||
if err := d.Send(m.msg); err != nil {
|
||||
// Check if this is an SMTP RESET error after successful delivery
|
||||
var sendErr *mail.SendError
|
||||
var errParsed = errors.As(err, &sendErr)
|
||||
if errParsed && sendErr.Reason == mail.ErrSMTPReset {
|
||||
open = false
|
||||
l.Debug("SMTP RESET error, closing connection...")
|
||||
// https://github.com/wneessen/go-mail/issues/463
|
||||
continue // Don't treat this as a delivery failure since mail was sent
|
||||
}
|
||||
|
||||
l.Warning("Failed to send email: %s, Cid=%s", err, m.cid)
|
||||
} else {
|
||||
l.Info("Email sent to %q, title: %q.", m.msg.GetHeader("To"), m.msg.GetHeader("Subject"))
|
||||
l.Info("Email sent to %q, title: %q.", m.to, m.subject)
|
||||
}
|
||||
// 长时间没有新邮件,则关闭SMTP连接
|
||||
case <-time.After(time.Duration(client.config.Keepalive) * time.Second):
|
||||
if open {
|
||||
if err := s.Close(); err != nil {
|
||||
if err := d.Close(); err != nil {
|
||||
client.l.Warning("Failed to close SMTP connection: %s", err)
|
||||
}
|
||||
open = false
|
||||
|
||||
@@ -38,18 +38,29 @@ func NewResetEmail(ctx context.Context, settings setting.Provider, user *ent.Use
|
||||
Url: url,
|
||||
}
|
||||
|
||||
tmpl, err := template.New("reset").Parse(selected.Body)
|
||||
tmplTitle, err := template.New("resetTitle").Parse(selected.Title)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to parse email title: %w", err)
|
||||
}
|
||||
|
||||
var resTitle strings.Builder
|
||||
err = tmplTitle.Execute(&resTitle, resetCtx)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to execute email title: %w", err)
|
||||
}
|
||||
|
||||
tmplBody, err := template.New("resetBody").Parse(selected.Body)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to parse email template: %w", err)
|
||||
}
|
||||
|
||||
var res strings.Builder
|
||||
err = tmpl.Execute(&res, resetCtx)
|
||||
var resBody strings.Builder
|
||||
err = tmplBody.Execute(&resBody, resetCtx)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to execute email template: %w", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[%s] %s", resetCtx.SiteBasic.Name, selected.Title), res.String(), nil
|
||||
return resTitle.String(), resBody.String(), nil
|
||||
}
|
||||
|
||||
// ActivationContext used for variables in activation email
|
||||
@@ -73,18 +84,29 @@ func NewActivationEmail(ctx context.Context, settings setting.Provider, user *en
|
||||
Url: url,
|
||||
}
|
||||
|
||||
tmpl, err := template.New("activation").Parse(selected.Body)
|
||||
tmplTitle, err := template.New("activationTitle").Parse(selected.Title)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to parse email title: %w", err)
|
||||
}
|
||||
|
||||
var resTitle strings.Builder
|
||||
err = tmplTitle.Execute(&resTitle, activationCtx)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to execute email title: %w", err)
|
||||
}
|
||||
|
||||
tmplBody, err := template.New("activationBody").Parse(selected.Body)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to parse email template: %w", err)
|
||||
}
|
||||
|
||||
var res strings.Builder
|
||||
err = tmpl.Execute(&res, activationCtx)
|
||||
var resBody strings.Builder
|
||||
err = tmplBody.Execute(&resBody, activationCtx)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to execute email template: %w", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[%s] %s", activationCtx.SiteBasic.Name, selected.Title), res.String(), nil
|
||||
return resTitle.String(), resBody.String(), nil
|
||||
}
|
||||
|
||||
func commonContext(ctx context.Context, settings setting.Provider) *CommonContext {
|
||||
@@ -122,4 +144,4 @@ func selectTemplate(templates []setting.EmailTemplate, u *ent.User) setting.Emai
|
||||
}
|
||||
|
||||
return selected
|
||||
}
|
||||
}
|
||||
@@ -244,7 +244,7 @@ func (handler *Driver) Put(ctx context.Context, file *fs.UploadRequest) error {
|
||||
|
||||
mimeType := file.Props.MimeType
|
||||
if mimeType == "" {
|
||||
handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
mimeType = handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
}
|
||||
|
||||
// 是否允许覆盖
|
||||
@@ -352,6 +352,14 @@ func (handler Driver) Thumb(ctx context.Context, expire *time.Time, ext string,
|
||||
w, h := handler.settings.ThumbSize(ctx)
|
||||
thumbParam := fmt.Sprintf("imageMogr2/thumbnail/%dx%d", w, h)
|
||||
|
||||
enco := handler.settings.ThumbEncode(ctx)
|
||||
switch enco.Format {
|
||||
case "jpg", "webp":
|
||||
thumbParam += fmt.Sprintf("/format/%s/rquality/%d", enco.Format, enco.Quality)
|
||||
case "png":
|
||||
thumbParam += fmt.Sprintf("/format/%s", enco.Format)
|
||||
}
|
||||
|
||||
source, err := handler.signSourceURL(
|
||||
ctx,
|
||||
e.Source(),
|
||||
@@ -374,7 +382,12 @@ func (handler Driver) Thumb(ctx context.Context, expire *time.Time, ext string,
|
||||
func (handler Driver) Source(ctx context.Context, e fs.Entity, args *driver.GetSourceArgs) (string, error) {
|
||||
// 添加各项设置
|
||||
options := urlOption{}
|
||||
|
||||
if args.Speed > 0 {
|
||||
// Byte 转换为 bit
|
||||
args.Speed *= 8
|
||||
|
||||
// COS对速度值有范围限制
|
||||
if args.Speed < 819200 {
|
||||
args.Speed = 819200
|
||||
}
|
||||
@@ -383,6 +396,7 @@ func (handler Driver) Source(ctx context.Context, e fs.Entity, args *driver.GetS
|
||||
}
|
||||
options.Speed = args.Speed
|
||||
}
|
||||
|
||||
if args.IsDownload {
|
||||
encodedFilename := url.PathEscape(args.DisplayName)
|
||||
options.ContentDescription = fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`,
|
||||
@@ -441,7 +455,7 @@ func (handler Driver) Token(ctx context.Context, uploadSession *fs.UploadSession
|
||||
|
||||
mimeType := file.Props.MimeType
|
||||
if mimeType == "" {
|
||||
handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
mimeType = handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
}
|
||||
|
||||
// 初始化分片上传
|
||||
@@ -580,7 +594,7 @@ func (handler Driver) Meta(ctx context.Context, path string) (*MetaData, error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) {
|
||||
func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) {
|
||||
if util.ContainsString(supportedImageExt, ext) {
|
||||
return handler.extractImageMeta(ctx, path)
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ type (
|
||||
Capabilities() *Capabilities
|
||||
|
||||
// MediaMeta extracts media metadata from the given file.
|
||||
MediaMeta(ctx context.Context, path, ext string) ([]MediaMeta, error)
|
||||
MediaMeta(ctx context.Context, path, ext, language string) ([]MediaMeta, error)
|
||||
}
|
||||
|
||||
Capabilities struct {
|
||||
@@ -117,6 +117,7 @@ const (
|
||||
MetaTypeExif MetaType = "exif"
|
||||
MediaTypeMusic MetaType = "music"
|
||||
MetaTypeStreamMedia MetaType = "stream"
|
||||
MetaTypeGeocoding MetaType = "geocoding"
|
||||
)
|
||||
|
||||
type ForceUsePublicEndpointCtx struct{}
|
||||
|
||||
@@ -219,7 +219,7 @@ func (handler *Driver) Put(ctx context.Context, file *fs.UploadRequest) error {
|
||||
|
||||
mimeType := file.Props.MimeType
|
||||
if mimeType == "" {
|
||||
handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
mimeType = handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
}
|
||||
|
||||
_, err := uploader.UploadWithContext(ctx, &s3manager.UploadInput{
|
||||
@@ -298,7 +298,48 @@ func (handler *Driver) Delete(ctx context.Context, files ...string) ([]string, e
|
||||
|
||||
// Thumb 获取缩略图URL
|
||||
func (handler *Driver) Thumb(ctx context.Context, expire *time.Time, ext string, e fs.Entity) (string, error) {
|
||||
return "", errors.New("not implemented")
|
||||
w, h := handler.settings.ThumbSize(ctx)
|
||||
thumbParam := fmt.Sprintf("@base@tag=imgScale&m=0&w=%d&h=%d", w, h)
|
||||
|
||||
enco := handler.settings.ThumbEncode(ctx)
|
||||
switch enco.Format {
|
||||
case "jpg", "webp":
|
||||
thumbParam += fmt.Sprintf("&q=%d&F=%s", enco.Quality, enco.Format)
|
||||
case "png":
|
||||
thumbParam += fmt.Sprintf("&F=%s", enco.Format)
|
||||
}
|
||||
|
||||
// 确保过期时间不小于 0 ,如果小于则设置为 7 天
|
||||
var ttl int64
|
||||
if expire != nil {
|
||||
ttl = int64(time.Until(*expire).Seconds())
|
||||
} else {
|
||||
ttl = 604800
|
||||
}
|
||||
|
||||
thumbUrl, err := handler.svc.GeneratePresignedUrl(&s3.GeneratePresignedUrlInput{
|
||||
HTTPMethod: s3.GET, // 请求方法
|
||||
Bucket: &handler.policy.BucketName, // 存储空间名称
|
||||
Key: aws.String(e.Source() + thumbParam), // 对象的key
|
||||
Expires: ttl, // 过期时间,转换为秒数
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 将最终生成的签名URL域名换成用户自定义的加速域名(如果有)
|
||||
finalThumbURL, err := url.Parse(thumbUrl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 公有空间替换掉Key及不支持的头
|
||||
if !handler.policy.IsPrivate {
|
||||
finalThumbURL.RawQuery = ""
|
||||
}
|
||||
|
||||
return finalThumbURL.String(), nil
|
||||
}
|
||||
|
||||
// Source 获取文件外链
|
||||
@@ -358,7 +399,7 @@ func (handler *Driver) Token(ctx context.Context, uploadSession *fs.UploadSessio
|
||||
|
||||
mimeType := file.Props.MimeType
|
||||
if mimeType == "" {
|
||||
handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
mimeType = handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
}
|
||||
|
||||
// 创建分片上传
|
||||
@@ -464,7 +505,7 @@ func (handler *Driver) Capabilities() *driver.Capabilities {
|
||||
}
|
||||
|
||||
// MediaMeta 获取媒体元信息
|
||||
func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) {
|
||||
func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
|
||||
@@ -140,9 +140,9 @@ func (handler *Driver) Put(ctx context.Context, file *fs.UploadRequest) error {
|
||||
}
|
||||
|
||||
openMode := os.O_CREATE | os.O_RDWR
|
||||
if file.Mode&fs.ModeOverwrite == fs.ModeOverwrite && file.Offset == 0 {
|
||||
openMode |= os.O_TRUNC
|
||||
}
|
||||
// if file.Mode&fs.ModeOverwrite == fs.ModeOverwrite && file.Offset == 0 {
|
||||
// openMode |= os.O_TRUNC
|
||||
// }
|
||||
|
||||
out, err := os.OpenFile(dst, openMode, Perm)
|
||||
if err != nil {
|
||||
@@ -298,6 +298,6 @@ func (handler *Driver) Capabilities() *driver.Capabilities {
|
||||
return capabilities
|
||||
}
|
||||
|
||||
func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) {
|
||||
func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func (d *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) {
|
||||
func (d *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) {
|
||||
thumbURL, err := d.signSourceURL(&obs.CreateSignedUrlInput{
|
||||
Method: obs.HttpMethodGet,
|
||||
Bucket: d.policy.BucketName,
|
||||
|
||||
@@ -335,13 +335,23 @@ func (d *Driver) LocalPath(ctx context.Context, path string) string {
|
||||
|
||||
func (d *Driver) Thumb(ctx context.Context, expire *time.Time, ext string, e fs.Entity) (string, error) {
|
||||
w, h := d.settings.ThumbSize(ctx)
|
||||
thumbParam := fmt.Sprintf("image/resize,m_lfit,w_%d,h_%d", w, h)
|
||||
|
||||
enco := d.settings.ThumbEncode(ctx)
|
||||
switch enco.Format {
|
||||
case "jpg", "webp":
|
||||
thumbParam += fmt.Sprintf("/format,%s/quality,q_%d", enco.Format, enco.Quality)
|
||||
case "png":
|
||||
thumbParam += fmt.Sprintf("/format,%s", enco.Format)
|
||||
}
|
||||
|
||||
thumbURL, err := d.signSourceURL(&obs.CreateSignedUrlInput{
|
||||
Method: obs.HttpMethodGet,
|
||||
Bucket: d.policy.BucketName,
|
||||
Key: e.Source(),
|
||||
Expires: int(time.Until(*expire).Seconds()),
|
||||
QueryParams: map[string]string{
|
||||
imageProcessHeader: fmt.Sprintf("image/resize,m_lfit,w_%d,h_%d", w, h),
|
||||
imageProcessHeader: thumbParam,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -241,7 +241,7 @@ func (handler *Driver) Capabilities() *driver.Capabilities {
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) {
|
||||
func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
|
||||
@@ -5,16 +5,17 @@ import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/mediameta"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/request"
|
||||
"github.com/samber/lo"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/mediameta"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/request"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -265,13 +266,14 @@ func (handler *Driver) extractImageMeta(ctx context.Context, path string) ([]dri
|
||||
|
||||
// extractMediaInfo Sends API calls to OSS IMM service to extract media info.
|
||||
func (handler *Driver) extractMediaInfo(ctx context.Context, path string, category string, forceSign bool) (string, error) {
|
||||
mediaOption := []oss.Option{oss.Process(category)}
|
||||
mediaInfoExpire := time.Now().Add(mediaInfoTTL)
|
||||
thumbURL, err := handler.signSourceURL(
|
||||
ctx,
|
||||
path,
|
||||
&mediaInfoExpire,
|
||||
mediaOption,
|
||||
&oss.GetObjectRequest{
|
||||
Process: oss.Ptr(category),
|
||||
},
|
||||
forceSign,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -15,7 +15,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
|
||||
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/boolset"
|
||||
@@ -52,7 +53,6 @@ type Driver struct {
|
||||
policy *ent.StoragePolicy
|
||||
|
||||
client *oss.Client
|
||||
bucket *oss.Bucket
|
||||
settings setting.Provider
|
||||
l logging.Logger
|
||||
config conf.ConfigProvider
|
||||
@@ -102,21 +102,27 @@ func New(ctx context.Context, policy *ent.StoragePolicy, settings setting.Provid
|
||||
|
||||
// CORS 创建跨域策略
|
||||
func (handler *Driver) CORS() error {
|
||||
return handler.client.SetBucketCORS(handler.policy.BucketName, []oss.CORSRule{
|
||||
{
|
||||
AllowedOrigin: []string{"*"},
|
||||
AllowedMethod: []string{
|
||||
"GET",
|
||||
"POST",
|
||||
"PUT",
|
||||
"DELETE",
|
||||
"HEAD",
|
||||
_, err := handler.client.PutBucketCors(context.Background(), &oss.PutBucketCorsRequest{
|
||||
Bucket: &handler.policy.BucketName,
|
||||
CORSConfiguration: &oss.CORSConfiguration{
|
||||
CORSRules: []oss.CORSRule{
|
||||
{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{
|
||||
"GET",
|
||||
"POST",
|
||||
"PUT",
|
||||
"DELETE",
|
||||
"HEAD",
|
||||
},
|
||||
ExposeHeaders: []string{},
|
||||
AllowedHeaders: []string{"*"},
|
||||
MaxAgeSeconds: oss.Ptr(int64(3600)),
|
||||
},
|
||||
},
|
||||
ExposeHeader: []string{},
|
||||
AllowedHeader: []string{"*"},
|
||||
MaxAgeSeconds: 3600,
|
||||
},
|
||||
})
|
||||
}})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// InitOSSClient 初始化OSS鉴权客户端
|
||||
@@ -125,34 +131,28 @@ func (handler *Driver) InitOSSClient(forceUsePublicEndpoint bool) error {
|
||||
return errors.New("empty policy")
|
||||
}
|
||||
|
||||
opt := make([]oss.ClientOption, 0)
|
||||
|
||||
// 决定是否使用内网 Endpoint
|
||||
endpoint := handler.policy.Server
|
||||
useCname := false
|
||||
if handler.policy.Settings.ServerSideEndpoint != "" && !forceUsePublicEndpoint {
|
||||
endpoint = handler.policy.Settings.ServerSideEndpoint
|
||||
} else if handler.policy.Settings.UseCname {
|
||||
opt = append(opt, oss.UseCname(true))
|
||||
useCname = true
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(endpoint, "http://") && !strings.HasPrefix(endpoint, "https://") {
|
||||
endpoint = "https://" + endpoint
|
||||
}
|
||||
|
||||
cfg := oss.LoadDefaultConfig().
|
||||
WithCredentialsProvider(credentials.NewStaticCredentialsProvider(handler.policy.AccessKey, handler.policy.SecretKey, "")).
|
||||
WithEndpoint(endpoint).
|
||||
WithRegion(handler.policy.Settings.Region).
|
||||
WithUseCName(useCname)
|
||||
|
||||
// 初始化客户端
|
||||
client, err := oss.New(endpoint, handler.policy.AccessKey, handler.policy.SecretKey, opt...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := oss.NewClient(cfg)
|
||||
handler.client = client
|
||||
|
||||
// 初始化存储桶
|
||||
bucket, err := client.Bucket(handler.policy.BucketName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
handler.bucket = bucket
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -166,38 +166,40 @@ func (handler *Driver) List(ctx context.Context, base string, onProgress driver.
|
||||
|
||||
var (
|
||||
delimiter string
|
||||
marker string
|
||||
objects []oss.ObjectProperties
|
||||
commons []string
|
||||
commons []oss.CommonPrefix
|
||||
)
|
||||
if !recursive {
|
||||
delimiter = "/"
|
||||
}
|
||||
|
||||
for {
|
||||
subRes, err := handler.bucket.ListObjects(oss.Marker(marker), oss.Prefix(base),
|
||||
oss.MaxKeys(1000), oss.Delimiter(delimiter))
|
||||
p := handler.client.NewListObjectsPaginator(&oss.ListObjectsRequest{
|
||||
Bucket: &handler.policy.BucketName,
|
||||
Prefix: &base,
|
||||
MaxKeys: 1000,
|
||||
Delimiter: &delimiter,
|
||||
})
|
||||
|
||||
for p.HasNext() {
|
||||
page, err := p.NextPage(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
objects = append(objects, subRes.Objects...)
|
||||
commons = append(commons, subRes.CommonPrefixes...)
|
||||
marker = subRes.NextMarker
|
||||
if marker == "" {
|
||||
break
|
||||
}
|
||||
|
||||
objects = append(objects, page.Contents...)
|
||||
commons = append(commons, page.CommonPrefixes...)
|
||||
}
|
||||
|
||||
// 处理列取结果
|
||||
res := make([]fs.PhysicalObject, 0, len(objects)+len(commons))
|
||||
// 处理目录
|
||||
for _, object := range commons {
|
||||
rel, err := filepath.Rel(base, object)
|
||||
rel, err := filepath.Rel(base, *object.Prefix)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
res = append(res, fs.PhysicalObject{
|
||||
Name: path.Base(object),
|
||||
Name: path.Base(*object.Prefix),
|
||||
RelativePath: filepath.ToSlash(rel),
|
||||
Size: 0,
|
||||
IsDir: true,
|
||||
@@ -208,17 +210,17 @@ func (handler *Driver) List(ctx context.Context, base string, onProgress driver.
|
||||
|
||||
// 处理文件
|
||||
for _, object := range objects {
|
||||
rel, err := filepath.Rel(base, object.Key)
|
||||
rel, err := filepath.Rel(base, *object.Key)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
res = append(res, fs.PhysicalObject{
|
||||
Name: path.Base(object.Key),
|
||||
Source: object.Key,
|
||||
Name: path.Base(*object.Key),
|
||||
Source: *object.Key,
|
||||
RelativePath: filepath.ToSlash(rel),
|
||||
Size: object.Size,
|
||||
IsDir: false,
|
||||
LastModify: object.LastModified,
|
||||
LastModify: *object.LastModified,
|
||||
})
|
||||
}
|
||||
onProgress(len(res))
|
||||
@@ -240,30 +242,39 @@ func (handler *Driver) Put(ctx context.Context, file *fs.UploadRequest) error {
|
||||
|
||||
mimeType := file.Props.MimeType
|
||||
if mimeType == "" {
|
||||
handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
mimeType = handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
}
|
||||
|
||||
// 是否允许覆盖
|
||||
overwrite := file.Mode&fs.ModeOverwrite == fs.ModeOverwrite
|
||||
options := []oss.Option{
|
||||
oss.WithContext(ctx),
|
||||
oss.Expires(time.Now().Add(credentialTTL * time.Second)),
|
||||
oss.ForbidOverWrite(!overwrite),
|
||||
oss.ContentType(mimeType),
|
||||
}
|
||||
forbidOverwrite := oss.Ptr(strconv.FormatBool(!overwrite))
|
||||
exipires := oss.Ptr(time.Now().Add(credentialTTL * time.Second).Format(time.RFC3339))
|
||||
|
||||
// 小文件直接上传
|
||||
if file.Props.Size < MultiPartUploadThreshold {
|
||||
return handler.bucket.PutObject(file.Props.SavePath, file, options...)
|
||||
_, err := handler.client.PutObject(ctx, &oss.PutObjectRequest{
|
||||
Bucket: &handler.policy.BucketName,
|
||||
Key: &file.Props.SavePath,
|
||||
Body: file,
|
||||
ForbidOverwrite: forbidOverwrite,
|
||||
ContentType: oss.Ptr(mimeType),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// 超过阈值时使用分片上传
|
||||
imur, err := handler.bucket.InitiateMultipartUpload(file.Props.SavePath, options...)
|
||||
imur, err := handler.client.InitiateMultipartUpload(ctx, &oss.InitiateMultipartUploadRequest{
|
||||
Bucket: &handler.policy.BucketName,
|
||||
Key: &file.Props.SavePath,
|
||||
ContentType: oss.Ptr(mimeType),
|
||||
ForbidOverwrite: forbidOverwrite,
|
||||
Expires: exipires,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initiate multipart upload: %w", err)
|
||||
}
|
||||
|
||||
parts := make([]oss.UploadPart, 0)
|
||||
parts := make([]*oss.UploadPartResult, 0)
|
||||
|
||||
chunks := chunk.NewChunkGroup(file, handler.chunkSize, &backoff.ConstantBackoff{
|
||||
Max: handler.settings.ChunkRetryLimit(ctx),
|
||||
@@ -271,7 +282,13 @@ func (handler *Driver) Put(ctx context.Context, file *fs.UploadRequest) error {
|
||||
}, handler.settings.UseChunkBuffer(ctx), handler.l, handler.settings.TempPath(ctx))
|
||||
|
||||
uploadFunc := func(current *chunk.ChunkGroup, content io.Reader) error {
|
||||
part, err := handler.bucket.UploadPart(imur, content, current.Length(), current.Index()+1, oss.WithContext(ctx))
|
||||
part, err := handler.client.UploadPart(ctx, &oss.UploadPartRequest{
|
||||
Bucket: &handler.policy.BucketName,
|
||||
Key: &file.Props.SavePath,
|
||||
UploadId: imur.UploadId,
|
||||
PartNumber: int32(current.Index() + 1),
|
||||
Body: content,
|
||||
})
|
||||
if err == nil {
|
||||
parts = append(parts, part)
|
||||
}
|
||||
@@ -280,14 +297,27 @@ func (handler *Driver) Put(ctx context.Context, file *fs.UploadRequest) error {
|
||||
|
||||
for chunks.Next() {
|
||||
if err := chunks.Process(uploadFunc); err != nil {
|
||||
handler.cancelUpload(imur)
|
||||
handler.cancelUpload(*imur)
|
||||
return fmt.Errorf("failed to upload chunk #%d: %w", chunks.Index(), err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = handler.bucket.CompleteMultipartUpload(imur, parts, oss.ForbidOverWrite(!overwrite), oss.WithContext(ctx))
|
||||
_, err = handler.client.CompleteMultipartUpload(ctx, &oss.CompleteMultipartUploadRequest{
|
||||
Bucket: &handler.policy.BucketName,
|
||||
Key: imur.Key,
|
||||
UploadId: imur.UploadId,
|
||||
CompleteMultipartUpload: &oss.CompleteMultipartUpload{
|
||||
Parts: lo.Map(parts, func(part *oss.UploadPartResult, i int) oss.UploadPart {
|
||||
return oss.UploadPart{
|
||||
PartNumber: int32(i + 1),
|
||||
ETag: part.ETag,
|
||||
}
|
||||
}),
|
||||
},
|
||||
ForbidOverwrite: oss.Ptr(strconv.FormatBool(!overwrite)),
|
||||
})
|
||||
if err != nil {
|
||||
handler.cancelUpload(imur)
|
||||
handler.cancelUpload(*imur)
|
||||
}
|
||||
|
||||
return err
|
||||
@@ -302,7 +332,12 @@ func (handler *Driver) Delete(ctx context.Context, files ...string) ([]string, e
|
||||
for index, group := range groups {
|
||||
handler.l.Debug("Process delete group #%d: %v", index, group)
|
||||
// 删除文件
|
||||
delRes, err := handler.bucket.DeleteObjects(group)
|
||||
delRes, err := handler.client.DeleteMultipleObjects(ctx, &oss.DeleteMultipleObjectsRequest{
|
||||
Bucket: &handler.policy.BucketName,
|
||||
Objects: lo.Map(group, func(v string, i int) oss.DeleteObject {
|
||||
return oss.DeleteObject{Key: &v}
|
||||
}),
|
||||
})
|
||||
if err != nil {
|
||||
failed = append(failed, group...)
|
||||
lastError = err
|
||||
@@ -310,7 +345,14 @@ func (handler *Driver) Delete(ctx context.Context, files ...string) ([]string, e
|
||||
}
|
||||
|
||||
// 统计未删除的文件
|
||||
failed = append(failed, util.SliceDifference(files, delRes.DeletedObjects)...)
|
||||
failed = append(
|
||||
failed,
|
||||
util.SliceDifference(files,
|
||||
lo.Map(delRes.DeletedObjects, func(v oss.DeletedInfo, i int) string {
|
||||
return *v.Key
|
||||
}),
|
||||
)...,
|
||||
)
|
||||
}
|
||||
|
||||
if len(failed) > 0 && lastError == nil {
|
||||
@@ -334,12 +376,23 @@ func (handler *Driver) Thumb(ctx context.Context, expire *time.Time, ext string,
|
||||
|
||||
w, h := handler.settings.ThumbSize(ctx)
|
||||
thumbParam := fmt.Sprintf("image/resize,m_lfit,h_%d,w_%d", h, w)
|
||||
thumbOption := []oss.Option{oss.Process(thumbParam)}
|
||||
|
||||
enco := handler.settings.ThumbEncode(ctx)
|
||||
switch enco.Format {
|
||||
case "jpg", "webp":
|
||||
thumbParam += fmt.Sprintf("/format,%s/quality,q_%d", enco.Format, enco.Quality)
|
||||
case "png":
|
||||
thumbParam += fmt.Sprintf("/format,%s", enco.Format)
|
||||
}
|
||||
|
||||
req := &oss.GetObjectRequest{
|
||||
Process: oss.Ptr(thumbParam),
|
||||
}
|
||||
thumbURL, err := handler.signSourceURL(
|
||||
ctx,
|
||||
e.Source(),
|
||||
expire,
|
||||
thumbOption,
|
||||
req,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -361,11 +414,11 @@ func (handler *Driver) Source(ctx context.Context, e fs.Entity, args *driver.Get
|
||||
}
|
||||
|
||||
// 添加各项设置
|
||||
var signOptions = make([]oss.Option, 0, 2)
|
||||
req := &oss.GetObjectRequest{}
|
||||
if args.IsDownload {
|
||||
encodedFilename := url.PathEscape(args.DisplayName)
|
||||
signOptions = append(signOptions, oss.ResponseContentDisposition(fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`,
|
||||
encodedFilename, encodedFilename)))
|
||||
req.ResponseContentDisposition = oss.Ptr(fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`,
|
||||
encodedFilename, encodedFilename))
|
||||
}
|
||||
if args.Speed > 0 {
|
||||
// Byte 转换为 bit
|
||||
@@ -378,25 +431,33 @@ func (handler *Driver) Source(ctx context.Context, e fs.Entity, args *driver.Get
|
||||
if args.Speed > 838860800 {
|
||||
args.Speed = 838860800
|
||||
}
|
||||
signOptions = append(signOptions, oss.TrafficLimitParam(args.Speed))
|
||||
req.TrafficLimit = args.Speed
|
||||
}
|
||||
|
||||
return handler.signSourceURL(ctx, e.Source(), args.Expire, signOptions, false)
|
||||
return handler.signSourceURL(ctx, e.Source(), args.Expire, req, false)
|
||||
}
|
||||
|
||||
func (handler *Driver) signSourceURL(ctx context.Context, path string, expire *time.Time, options []oss.Option, forceSign bool) (string, error) {
|
||||
ttl := int64(86400 * 365 * 20)
|
||||
func (handler *Driver) signSourceURL(ctx context.Context, path string, expire *time.Time, req *oss.GetObjectRequest, forceSign bool) (string, error) {
|
||||
ttl := time.Duration(24) * time.Hour * 365 * 20
|
||||
if expire != nil {
|
||||
ttl = int64(time.Until(*expire).Seconds())
|
||||
ttl = time.Until(*expire)
|
||||
}
|
||||
|
||||
signedURL, err := handler.bucket.SignURL(path, oss.HTTPGet, ttl, options...)
|
||||
if req == nil {
|
||||
req = &oss.GetObjectRequest{}
|
||||
}
|
||||
|
||||
req.Bucket = &handler.policy.BucketName
|
||||
req.Key = &path
|
||||
|
||||
// signedURL, err := handler.client.Presign(path, oss.HTTPGet, ttl, options...)
|
||||
result, err := handler.client.Presign(ctx, req, oss.PresignExpires(ttl))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 将最终生成的签名URL域名换成用户自定义的加速域名(如果有)
|
||||
finalURL, err := url.Parse(signedURL)
|
||||
finalURL, err := url.Parse(result.URL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -441,38 +502,40 @@ func (handler *Driver) Token(ctx context.Context, uploadSession *fs.UploadSessio
|
||||
|
||||
mimeType := file.Props.MimeType
|
||||
if mimeType == "" {
|
||||
handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
mimeType = handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
}
|
||||
|
||||
// 初始化分片上传
|
||||
options := []oss.Option{
|
||||
oss.WithContext(ctx),
|
||||
oss.Expires(uploadSession.Props.ExpireAt),
|
||||
oss.ForbidOverWrite(true),
|
||||
oss.ContentType(mimeType),
|
||||
}
|
||||
imur, err := handler.bucket.InitiateMultipartUpload(file.Props.SavePath, options...)
|
||||
imur, err := handler.client.InitiateMultipartUpload(ctx, &oss.InitiateMultipartUploadRequest{
|
||||
Bucket: &handler.policy.BucketName,
|
||||
Key: &file.Props.SavePath,
|
||||
ContentType: oss.Ptr(mimeType),
|
||||
ForbidOverwrite: oss.Ptr(strconv.FormatBool(true)),
|
||||
Expires: oss.Ptr(uploadSession.Props.ExpireAt.Format(time.RFC3339)),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize multipart upload: %w", err)
|
||||
}
|
||||
uploadSession.UploadID = imur.UploadID
|
||||
uploadSession.UploadID = *imur.UploadId
|
||||
|
||||
// 为每个分片签名上传 URL
|
||||
chunks := chunk.NewChunkGroup(file, handler.chunkSize, &backoff.ConstantBackoff{}, false, handler.l, "")
|
||||
urls := make([]string, chunks.Num())
|
||||
ttl := int64(time.Until(uploadSession.Props.ExpireAt).Seconds())
|
||||
ttl := time.Until(uploadSession.Props.ExpireAt)
|
||||
for chunks.Next() {
|
||||
err := chunks.Process(func(c *chunk.ChunkGroup, chunk io.Reader) error {
|
||||
signedURL, err := handler.bucket.SignURL(file.Props.SavePath, oss.HTTPPut,
|
||||
ttl,
|
||||
oss.AddParam(partNumberParam, strconv.Itoa(c.Index()+1)),
|
||||
oss.AddParam(uploadIdParam, imur.UploadID),
|
||||
oss.ContentType("application/octet-stream"))
|
||||
signedURL, err := handler.client.Presign(ctx, &oss.UploadPartRequest{
|
||||
Bucket: &handler.policy.BucketName,
|
||||
Key: &file.Props.SavePath,
|
||||
UploadId: imur.UploadId,
|
||||
PartNumber: int32(c.Index() + 1),
|
||||
Body: chunk,
|
||||
}, oss.PresignExpires(ttl))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
urls[c.Index()] = signedURL
|
||||
urls[c.Index()] = signedURL.URL
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -481,21 +544,22 @@ func (handler *Driver) Token(ctx context.Context, uploadSession *fs.UploadSessio
|
||||
}
|
||||
|
||||
// 签名完成分片上传的URL
|
||||
completeURL, err := handler.bucket.SignURL(file.Props.SavePath, oss.HTTPPost, ttl,
|
||||
oss.ContentType("application/octet-stream"),
|
||||
oss.AddParam(uploadIdParam, imur.UploadID),
|
||||
oss.Expires(time.Now().Add(time.Duration(ttl)*time.Second)),
|
||||
oss.SetHeader(completeAllHeader, "yes"),
|
||||
oss.ForbidOverWrite(true),
|
||||
oss.AddParam(callbackParam, callbackPolicyEncoded))
|
||||
completeURL, err := handler.client.Presign(ctx, &oss.CompleteMultipartUploadRequest{
|
||||
Bucket: &handler.policy.BucketName,
|
||||
Key: &file.Props.SavePath,
|
||||
UploadId: imur.UploadId,
|
||||
CompleteAll: oss.Ptr("yes"),
|
||||
ForbidOverwrite: oss.Ptr(strconv.FormatBool(true)),
|
||||
Callback: oss.Ptr(callbackPolicyEncoded),
|
||||
}, oss.PresignExpires(ttl))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &fs.UploadCredential{
|
||||
UploadID: imur.UploadID,
|
||||
UploadID: *imur.UploadId,
|
||||
UploadURLs: urls,
|
||||
CompleteURL: completeURL,
|
||||
CompleteURL: completeURL.URL,
|
||||
SessionID: uploadSession.Props.UploadSessionID,
|
||||
ChunkSize: handler.chunkSize,
|
||||
}, nil
|
||||
@@ -503,7 +567,12 @@ func (handler *Driver) Token(ctx context.Context, uploadSession *fs.UploadSessio
|
||||
|
||||
// 取消上传凭证
|
||||
func (handler *Driver) CancelToken(ctx context.Context, uploadSession *fs.UploadSession) error {
|
||||
return handler.bucket.AbortMultipartUpload(oss.InitiateMultipartUploadResult{UploadID: uploadSession.UploadID, Key: uploadSession.Props.SavePath}, oss.WithContext(ctx))
|
||||
_, err := handler.client.AbortMultipartUpload(ctx, &oss.AbortMultipartUploadRequest{
|
||||
Bucket: &handler.policy.BucketName,
|
||||
Key: &uploadSession.Props.SavePath,
|
||||
UploadId: &uploadSession.UploadID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (handler *Driver) CompleteUpload(ctx context.Context, session *fs.UploadSession) error {
|
||||
@@ -526,7 +595,7 @@ func (handler *Driver) Capabilities() *driver.Capabilities {
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) {
|
||||
func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) {
|
||||
if util.ContainsString(supportedImageExt, ext) {
|
||||
return handler.extractImageMeta(ctx, path)
|
||||
}
|
||||
@@ -547,7 +616,11 @@ func (handler *Driver) LocalPath(ctx context.Context, path string) string {
|
||||
}
|
||||
|
||||
func (handler *Driver) cancelUpload(imur oss.InitiateMultipartUploadResult) {
|
||||
if err := handler.bucket.AbortMultipartUpload(imur); err != nil {
|
||||
if _, err := handler.client.AbortMultipartUpload(context.Background(), &oss.AbortMultipartUploadRequest{
|
||||
Bucket: &handler.policy.BucketName,
|
||||
Key: imur.Key,
|
||||
UploadId: imur.UploadId,
|
||||
}); err != nil {
|
||||
handler.l.Warning("failed to abort multipart upload: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ func (handler *Driver) Put(ctx context.Context, file *fs.UploadRequest) error {
|
||||
|
||||
mimeType := file.Props.MimeType
|
||||
if mimeType == "" {
|
||||
handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
mimeType = handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
}
|
||||
|
||||
err = resumeUploader.CompleteParts(ctx, upToken, upHost, nil, handler.policy.BucketName,
|
||||
@@ -277,10 +277,20 @@ func (handler *Driver) Delete(ctx context.Context, files ...string) ([]string, e
|
||||
// Thumb 获取文件缩略图
|
||||
func (handler *Driver) Thumb(ctx context.Context, expire *time.Time, ext string, e fs.Entity) (string, error) {
|
||||
w, h := handler.settings.ThumbSize(ctx)
|
||||
thumbParam := fmt.Sprintf("imageView2/1/w/%d/h/%d", w, h)
|
||||
|
||||
enco := handler.settings.ThumbEncode(ctx)
|
||||
switch enco.Format {
|
||||
case "jpg", "webp":
|
||||
thumbParam += fmt.Sprintf("/format/%s/q/%d", enco.Format, enco.Quality)
|
||||
case "png":
|
||||
thumbParam += fmt.Sprintf("/format/%s", enco.Format)
|
||||
}
|
||||
|
||||
return handler.signSourceURL(
|
||||
e.Source(),
|
||||
url.Values{
|
||||
fmt.Sprintf("imageView2/1/w/%d/h/%d", w, h): []string{},
|
||||
thumbParam: []string{},
|
||||
},
|
||||
expire,
|
||||
), nil
|
||||
@@ -379,7 +389,7 @@ func (handler *Driver) Token(ctx context.Context, uploadSession *fs.UploadSessio
|
||||
|
||||
mimeType := file.Props.MimeType
|
||||
if mimeType == "" {
|
||||
handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
mimeType = handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
}
|
||||
|
||||
uploadSession.UploadID = ret.UploadID
|
||||
@@ -423,7 +433,7 @@ func (handler *Driver) Capabilities() *driver.Capabilities {
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) {
|
||||
func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) {
|
||||
if util.ContainsString(supportedImageExt, ext) {
|
||||
return handler.extractImageMeta(ctx, path)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ type Client interface {
|
||||
// DeleteUploadSession deletes remote upload session
|
||||
DeleteUploadSession(ctx context.Context, sessionID string) error
|
||||
// MediaMeta gets media meta from remote server
|
||||
MediaMeta(ctx context.Context, src, ext string) ([]driver.MediaMeta, error)
|
||||
MediaMeta(ctx context.Context, src, ext, language string) ([]driver.MediaMeta, error)
|
||||
// DeleteFiles deletes files from remote server
|
||||
DeleteFiles(ctx context.Context, files ...string) ([]string, error)
|
||||
// List lists files from remote server
|
||||
@@ -183,10 +183,10 @@ func (c *remoteClient) DeleteFiles(ctx context.Context, files ...string) ([]stri
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *remoteClient) MediaMeta(ctx context.Context, src, ext string) ([]driver.MediaMeta, error) {
|
||||
func (c *remoteClient) MediaMeta(ctx context.Context, src, ext, language string) ([]driver.MediaMeta, error) {
|
||||
resp, err := c.httpClient.Request(
|
||||
http.MethodGet,
|
||||
routes.SlaveMediaMetaRoute(src, ext),
|
||||
routes.SlaveMediaMetaRoute(src, ext, language),
|
||||
nil,
|
||||
request.WithContext(ctx),
|
||||
request.WithLogger(c.l),
|
||||
|
||||
@@ -179,6 +179,6 @@ func (handler *Driver) Capabilities() *driver.Capabilities {
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) {
|
||||
return handler.uploadClient.MediaMeta(ctx, path, ext)
|
||||
func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) {
|
||||
return handler.uploadClient.MediaMeta(ctx, path, ext, language)
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ func (handler *Driver) Put(ctx context.Context, file *fs.UploadRequest) error {
|
||||
|
||||
mimeType := file.Props.MimeType
|
||||
if mimeType == "" {
|
||||
handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
mimeType = handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
}
|
||||
|
||||
_, err := uploader.UploadWithContext(ctx, &s3manager.UploadInput{
|
||||
@@ -344,7 +344,7 @@ func (handler *Driver) Token(ctx context.Context, uploadSession *fs.UploadSessio
|
||||
|
||||
mimeType := file.Props.MimeType
|
||||
if mimeType == "" {
|
||||
handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
mimeType = handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
}
|
||||
|
||||
// 创建分片上传
|
||||
@@ -482,7 +482,7 @@ func (handler *Driver) Capabilities() *driver.Capabilities {
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) {
|
||||
func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ func (handler *Driver) Put(ctx context.Context, file *fs.UploadRequest) error {
|
||||
|
||||
mimeType := file.Props.MimeType
|
||||
if mimeType == "" {
|
||||
handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
mimeType = handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
}
|
||||
|
||||
err := handler.up.Put(&upyun.PutObjectConfig{
|
||||
@@ -203,8 +203,16 @@ func (handler *Driver) Delete(ctx context.Context, files ...string) ([]string, e
|
||||
// Thumb 获取文件缩略图
|
||||
func (handler *Driver) Thumb(ctx context.Context, expire *time.Time, ext string, e fs.Entity) (string, error) {
|
||||
w, h := handler.settings.ThumbSize(ctx)
|
||||
|
||||
thumbParam := fmt.Sprintf("!/fwfh/%dx%d", w, h)
|
||||
|
||||
enco := handler.settings.ThumbEncode(ctx)
|
||||
switch enco.Format {
|
||||
case "jpg", "webp":
|
||||
thumbParam += fmt.Sprintf("/format/%s/quality/%d", enco.Format, enco.Quality)
|
||||
case "png":
|
||||
thumbParam += fmt.Sprintf("/format/%s", enco.Format)
|
||||
}
|
||||
|
||||
thumbURL, err := handler.signURL(ctx, e.Source()+thumbParam, nil, expire)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -301,7 +309,7 @@ func (handler *Driver) Token(ctx context.Context, uploadSession *fs.UploadSessio
|
||||
|
||||
mimeType := file.Props.MimeType
|
||||
if mimeType == "" {
|
||||
handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
mimeType = handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
}
|
||||
|
||||
return &fs.UploadCredential{
|
||||
@@ -337,7 +345,7 @@ func (handler *Driver) Capabilities() *driver.Capabilities {
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) {
|
||||
func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) {
|
||||
return handler.extractImageMeta(ctx, path)
|
||||
}
|
||||
|
||||
|
||||
@@ -760,7 +760,6 @@ func (f *DBFS) deleteFiles(ctx context.Context, targets map[Navigator][]*File, f
|
||||
if f.user.Edges.Group == nil {
|
||||
return nil, nil, fmt.Errorf("user group not loaded")
|
||||
}
|
||||
limit := max(f.user.Edges.Group.Settings.MaxWalkedFiles, 1)
|
||||
allStaleEntities := make([]fs.Entity, 0, len(targets))
|
||||
storageDiff := make(inventory.StorageDiff)
|
||||
for n, files := range targets {
|
||||
@@ -774,8 +773,7 @@ func (f *DBFS) deleteFiles(ctx context.Context, targets map[Navigator][]*File, f
|
||||
|
||||
// List all files to be deleted
|
||||
toBeDeletedFiles := make([]*File, 0, len(files))
|
||||
if err := n.Walk(ctx, files, limit, intsets.MaxInt, func(targets []*File, level int) error {
|
||||
limit -= len(targets)
|
||||
if err := n.Walk(ctx, files, intsets.MaxInt, intsets.MaxInt, func(targets []*File, level int) error {
|
||||
toBeDeletedFiles = append(toBeDeletedFiles, targets...)
|
||||
return nil
|
||||
}); err != nil {
|
||||
|
||||
@@ -157,6 +157,14 @@ func (n *shareNavigator) Root(ctx context.Context, path *fs.URI) (*File, error)
|
||||
}
|
||||
|
||||
if n.user.ID != n.owner.ID && !n.user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionShareDownload)) {
|
||||
if inventory.IsAnonymousUser(n.user) {
|
||||
return nil, serializer.NewError(
|
||||
serializer.CodeAnonymouseAccessDenied,
|
||||
fmt.Sprintf("You don't have permission to access share links"),
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
return nil, serializer.NewError(
|
||||
serializer.CodeNoPermissionErr,
|
||||
fmt.Sprintf("You don't have permission to access share links"),
|
||||
|
||||
@@ -146,11 +146,7 @@ func (f *DBFS) PrepareUpload(ctx context.Context, req *fs.UploadRequest, opts ..
|
||||
if req.Props.SavePath == "" || isThumbnailAndPolicyNotAvailable {
|
||||
req.Props.SavePath = generateSavePath(policy, req, f.user)
|
||||
if isThumbnailAndPolicyNotAvailable {
|
||||
req.Props.SavePath = fmt.Sprintf(
|
||||
"%s.%s%s",
|
||||
req.Props.SavePath,
|
||||
util.RandStringRunes(16),
|
||||
f.settingClient.ThumbEntitySuffix(ctx))
|
||||
req.Props.SavePath = req.Props.SavePath + f.settingClient.ThumbEntitySuffix(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -699,6 +699,8 @@ func LockSessionToContext(ctx context.Context, session LockSession) context.Cont
|
||||
return context.WithValue(ctx, LockSessionCtxKey{}, session)
|
||||
}
|
||||
|
||||
// FindDesiredEntity finds the desired entity from the file.
|
||||
// entityType is optional, if it is not nil, it will only return the entity with the given type.
|
||||
func FindDesiredEntity(file File, version string, hasher hashid.Encoder, entityType *types.EntityType) (bool, Entity) {
|
||||
if version == "" {
|
||||
return true, file.PrimaryEntity()
|
||||
|
||||
@@ -36,5 +36,11 @@ func (d *mimeDetector) TypeByName(p string) string {
|
||||
return m
|
||||
}
|
||||
|
||||
return mime.TypeByExtension(ext)
|
||||
m := mime.TypeByExtension(ext)
|
||||
if m != "" {
|
||||
return m
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
@@ -3,19 +3,153 @@ package manager
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bodgit/sevenzip"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"golang.org/x/text/encoding"
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
"golang.org/x/text/encoding/japanese"
|
||||
"golang.org/x/text/encoding/korean"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
"golang.org/x/text/encoding/traditionalchinese"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
"golang.org/x/tools/container/intsets"
|
||||
)
|
||||
|
||||
type (
|
||||
ArchivedFile struct {
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
UpdatedAt *time.Time `json:"updated_at"`
|
||||
IsDirectory bool `json:"is_directory"`
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
ArchiveListCacheTTL = 3600 // 1 hour
|
||||
)
|
||||
|
||||
func init() {
|
||||
gob.Register([]ArchivedFile{})
|
||||
}
|
||||
|
||||
var ZipEncodings = map[string]encoding.Encoding{
|
||||
"ibm866": charmap.CodePage866,
|
||||
"iso8859_2": charmap.ISO8859_2,
|
||||
"iso8859_3": charmap.ISO8859_3,
|
||||
"iso8859_4": charmap.ISO8859_4,
|
||||
"iso8859_5": charmap.ISO8859_5,
|
||||
"iso8859_6": charmap.ISO8859_6,
|
||||
"iso8859_7": charmap.ISO8859_7,
|
||||
"iso8859_8": charmap.ISO8859_8,
|
||||
"iso8859_8I": charmap.ISO8859_8I,
|
||||
"iso8859_10": charmap.ISO8859_10,
|
||||
"iso8859_13": charmap.ISO8859_13,
|
||||
"iso8859_14": charmap.ISO8859_14,
|
||||
"iso8859_15": charmap.ISO8859_15,
|
||||
"iso8859_16": charmap.ISO8859_16,
|
||||
"koi8r": charmap.KOI8R,
|
||||
"koi8u": charmap.KOI8U,
|
||||
"macintosh": charmap.Macintosh,
|
||||
"windows874": charmap.Windows874,
|
||||
"windows1250": charmap.Windows1250,
|
||||
"windows1251": charmap.Windows1251,
|
||||
"windows1252": charmap.Windows1252,
|
||||
"windows1253": charmap.Windows1253,
|
||||
"windows1254": charmap.Windows1254,
|
||||
"windows1255": charmap.Windows1255,
|
||||
"windows1256": charmap.Windows1256,
|
||||
"windows1257": charmap.Windows1257,
|
||||
"windows1258": charmap.Windows1258,
|
||||
"macintoshcyrillic": charmap.MacintoshCyrillic,
|
||||
"gbk": simplifiedchinese.GBK,
|
||||
"gb18030": simplifiedchinese.GB18030,
|
||||
"big5": traditionalchinese.Big5,
|
||||
"eucjp": japanese.EUCJP,
|
||||
"iso2022jp": japanese.ISO2022JP,
|
||||
"shiftjis": japanese.ShiftJIS,
|
||||
"euckr": korean.EUCKR,
|
||||
"utf16be": unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM),
|
||||
"utf16le": unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM),
|
||||
}
|
||||
|
||||
func (m *manager) ListArchiveFiles(ctx context.Context, uri *fs.URI, entity, zipEncoding string) ([]ArchivedFile, error) {
|
||||
file, err := m.fs.Get(ctx, uri, dbfs.WithFileEntities(), dbfs.WithRequiredCapabilities(dbfs.NavigatorCapabilityDownloadFile))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get file: %w", err)
|
||||
}
|
||||
|
||||
if file.Type() != types.FileTypeFile {
|
||||
return nil, fs.ErrNotSupportedAction.WithError(fmt.Errorf("path %s is not a file", uri))
|
||||
}
|
||||
|
||||
// Validate file size
|
||||
if m.user.Edges.Group.Settings.DecompressSize > 0 && file.Size() > m.user.Edges.Group.Settings.DecompressSize {
|
||||
return nil, fs.ErrFileSizeTooBig.WithError(fmt.Errorf("file size %d exceeds the limit %d", file.Size(), m.user.Edges.Group.Settings.DecompressSize))
|
||||
}
|
||||
|
||||
found, targetEntity := fs.FindDesiredEntity(file, entity, m.hasher, nil)
|
||||
if !found {
|
||||
return nil, fs.ErrEntityNotExist
|
||||
}
|
||||
|
||||
var (
|
||||
enc encoding.Encoding
|
||||
ok bool
|
||||
)
|
||||
if zipEncoding != "" {
|
||||
enc, ok = ZipEncodings[strings.ToLower(zipEncoding)]
|
||||
if !ok {
|
||||
return nil, fs.ErrNotSupportedAction.WithError(fmt.Errorf("not supported zip encoding: %s", zipEncoding))
|
||||
}
|
||||
}
|
||||
|
||||
cacheKey := getArchiveListCacheKey(targetEntity.ID(), zipEncoding)
|
||||
kv := m.kv
|
||||
res, found := kv.Get(cacheKey)
|
||||
if found {
|
||||
return res.([]ArchivedFile), nil
|
||||
}
|
||||
|
||||
es, err := m.GetEntitySource(ctx, 0, fs.WithEntity(targetEntity))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get entity source: %w", err)
|
||||
}
|
||||
|
||||
es.Apply(entitysource.WithContext(ctx))
|
||||
defer es.Close()
|
||||
|
||||
var readerFunc func(ctx context.Context, file io.ReaderAt, size int64, textEncoding encoding.Encoding) ([]ArchivedFile, error)
|
||||
switch file.Ext() {
|
||||
case "zip":
|
||||
readerFunc = getZipFileList
|
||||
case "7z":
|
||||
readerFunc = get7zFileList
|
||||
default:
|
||||
return nil, fs.ErrNotSupportedAction.WithError(fmt.Errorf("not supported archive format: %s", file.Ext()))
|
||||
}
|
||||
|
||||
sr := io.NewSectionReader(es, 0, targetEntity.Size())
|
||||
fileList, err := readerFunc(ctx, sr, targetEntity.Size(), enc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file list: %w", err)
|
||||
}
|
||||
|
||||
kv.Set(cacheKey, fileList, ArchiveListCacheTTL)
|
||||
return fileList, nil
|
||||
}
|
||||
|
||||
func (m *manager) CreateArchive(ctx context.Context, uris []*fs.URI, writer io.Writer, opts ...fs.Option) (int, error) {
|
||||
o := newOption()
|
||||
for _, opt := range opts {
|
||||
@@ -122,3 +256,62 @@ func (m *manager) compressFileToArchive(ctx context.Context, parent string, file
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
func getZipFileList(ctx context.Context, file io.ReaderAt, size int64, textEncoding encoding.Encoding) ([]ArchivedFile, error) {
|
||||
zr, err := zip.NewReader(file, size)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create zip reader: %w", err)
|
||||
}
|
||||
|
||||
fileList := make([]ArchivedFile, 0, len(zr.File))
|
||||
for _, f := range zr.File {
|
||||
hdr := f.FileHeader
|
||||
if hdr.NonUTF8 && textEncoding != nil {
|
||||
dec := textEncoding.NewDecoder()
|
||||
filename, err := dec.String(hdr.Name)
|
||||
if err == nil {
|
||||
hdr.Name = filename
|
||||
}
|
||||
if hdr.Comment != "" {
|
||||
comment, err := dec.String(hdr.Comment)
|
||||
if err == nil {
|
||||
hdr.Comment = comment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info := f.FileInfo()
|
||||
modTime := info.ModTime()
|
||||
fileList = append(fileList, ArchivedFile{
|
||||
Name: util.FormSlash(hdr.Name),
|
||||
Size: info.Size(),
|
||||
UpdatedAt: &modTime,
|
||||
IsDirectory: info.IsDir(),
|
||||
})
|
||||
}
|
||||
return fileList, nil
|
||||
}
|
||||
|
||||
func get7zFileList(ctx context.Context, file io.ReaderAt, size int64, extEncoding encoding.Encoding) ([]ArchivedFile, error) {
|
||||
zr, err := sevenzip.NewReader(file, size)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create 7z reader: %w", err)
|
||||
}
|
||||
|
||||
fileList := make([]ArchivedFile, 0, len(zr.File))
|
||||
for _, f := range zr.File {
|
||||
info := f.FileInfo()
|
||||
modTime := info.ModTime()
|
||||
fileList = append(fileList, ArchivedFile{
|
||||
Name: util.FormSlash(f.Name),
|
||||
Size: info.Size(),
|
||||
UpdatedAt: &modTime,
|
||||
IsDirectory: info.IsDir(),
|
||||
})
|
||||
}
|
||||
return fileList, nil
|
||||
}
|
||||
|
||||
func getArchiveListCacheKey(entity int, encoding string) string {
|
||||
return fmt.Sprintf("archive_list_%d_%s", entity, encoding)
|
||||
}
|
||||
|
||||
@@ -163,6 +163,10 @@ type (
|
||||
rsc io.ReadCloser
|
||||
pos int64
|
||||
o *EntitySourceOptions
|
||||
|
||||
// Cache for resetRequest URL and expiry
|
||||
cachedUrl string
|
||||
cachedExpiry time.Time
|
||||
}
|
||||
)
|
||||
|
||||
@@ -215,6 +219,10 @@ func NewEntitySource(
|
||||
}
|
||||
|
||||
func (f *entitySource) Apply(opts ...EntitySourceOption) {
|
||||
if len(opts) > 0 {
|
||||
// Clear cache when options are applied as they might affect URL generation
|
||||
f.clearUrlCache()
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.Apply(f.o)
|
||||
}
|
||||
@@ -247,6 +255,10 @@ func (f *entitySource) LocalPath(ctx context.Context) string {
|
||||
}
|
||||
|
||||
func (f *entitySource) Serve(w http.ResponseWriter, r *http.Request, opts ...EntitySourceOption) {
|
||||
if len(opts) > 0 {
|
||||
// Clear cache when options are applied as they might affect URL generation
|
||||
f.clearUrlCache()
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.Apply(f.o)
|
||||
}
|
||||
@@ -478,16 +490,22 @@ func (f *entitySource) Read(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (f *entitySource) ReadAt(p []byte, off int64) (n int, err error) {
|
||||
if f.IsLocal() {
|
||||
if f.rsc == nil {
|
||||
err = f.resetRequest()
|
||||
}
|
||||
if readAt, ok := f.rsc.(io.ReaderAt); ok {
|
||||
return readAt.ReadAt(p, off)
|
||||
if f.rsc == nil {
|
||||
err = f.resetRequest()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
if readAt, ok := f.rsc.(io.ReaderAt); ok {
|
||||
return readAt.ReadAt(p, off)
|
||||
}
|
||||
|
||||
return 0, errors.New("source does not support ReadAt")
|
||||
// For non-local sources, use HTTP range request to read at specific offset
|
||||
rsc, err := f.getRsc(off)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return io.ReadFull(rsc, p)
|
||||
}
|
||||
|
||||
func (f *entitySource) Seek(offset int64, whence int) (int64, error) {
|
||||
@@ -524,6 +542,12 @@ func (f *entitySource) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearUrlCache clears the cached URL and expiry
|
||||
func (f *entitySource) clearUrlCache() {
|
||||
f.cachedUrl = ""
|
||||
f.cachedExpiry = time.Time{}
|
||||
}
|
||||
|
||||
func (f *entitySource) ShouldInternalProxy(opts ...EntitySourceOption) bool {
|
||||
for _, opt := range opts {
|
||||
opt.Apply(f.o)
|
||||
@@ -534,6 +558,10 @@ func (f *entitySource) ShouldInternalProxy(opts ...EntitySourceOption) bool {
|
||||
}
|
||||
|
||||
func (f *entitySource) Url(ctx context.Context, opts ...EntitySourceOption) (*EntityUrl, error) {
|
||||
if len(opts) > 0 {
|
||||
// Clear cache when options are applied as they might affect URL generation
|
||||
f.clearUrlCache()
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.Apply(f.o)
|
||||
}
|
||||
@@ -613,50 +641,75 @@ func (f *entitySource) Url(ctx context.Context, opts ...EntitySourceOption) (*En
|
||||
|
||||
func (f *entitySource) resetRequest() error {
|
||||
// For inbound files, we can use the handler to open the file directly
|
||||
if f.IsLocal() {
|
||||
if f.rsc == nil {
|
||||
file, err := f.handler.Open(f.o.Ctx, f.e.Source())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open inbound file: %w", err)
|
||||
}
|
||||
|
||||
if f.pos > 0 {
|
||||
_, err = file.Seek(f.pos, io.SeekStart)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to seek inbound file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
f.rsc = file
|
||||
|
||||
if f.o.SpeedLimit > 0 {
|
||||
bucket := ratelimit.NewBucketWithRate(float64(f.o.SpeedLimit), f.o.SpeedLimit)
|
||||
f.rsc = lrs{f.rsc, ratelimit.Reader(f.rsc, bucket)}
|
||||
}
|
||||
}
|
||||
|
||||
if f.IsLocal() && f.rsc != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
expire := time.Now().Add(defaultUrlExpire)
|
||||
u, err := f.Url(driver.WithForcePublicEndpoint(f.o.Ctx, false), WithNoInternalProxy(), WithExpire(&expire))
|
||||
rsc, err := f.getRsc(f.pos)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate download url: %w", err)
|
||||
return fmt.Errorf("failed to get rsc: %w", err)
|
||||
}
|
||||
f.rsc = rsc
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *entitySource) getRsc(pos int64) (io.ReadCloser, error) {
|
||||
// For inbound files, we can use the handler to open the file directly
|
||||
if f.IsLocal() {
|
||||
file, err := f.handler.Open(f.o.Ctx, f.e.Source())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open inbound file: %w", err)
|
||||
}
|
||||
|
||||
if pos > 0 {
|
||||
_, err = file.Seek(pos, io.SeekStart)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to seek inbound file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if f.o.SpeedLimit > 0 {
|
||||
bucket := ratelimit.NewBucketWithRate(float64(f.o.SpeedLimit), f.o.SpeedLimit)
|
||||
return lrs{file, ratelimit.Reader(file, bucket)}, nil
|
||||
} else {
|
||||
return file, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var urlStr string
|
||||
now := time.Now()
|
||||
|
||||
// Check if we have a valid cached URL and expiry
|
||||
if f.cachedUrl != "" && now.Before(f.cachedExpiry.Add(-time.Minute)) {
|
||||
// Use cached URL if it's still valid (with 1 minute buffer before expiry)
|
||||
urlStr = f.cachedUrl
|
||||
} else {
|
||||
// Generate new URL and cache it
|
||||
expire := now.Add(defaultUrlExpire)
|
||||
u, err := f.Url(driver.WithForcePublicEndpoint(f.o.Ctx, false), WithNoInternalProxy(), WithExpire(&expire))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate download url: %w", err)
|
||||
}
|
||||
|
||||
// Cache the URL and expiry
|
||||
f.cachedUrl = u.Url
|
||||
f.cachedExpiry = expire
|
||||
urlStr = u.Url
|
||||
}
|
||||
|
||||
h := http.Header{}
|
||||
h.Set("Range", fmt.Sprintf("bytes=%d-", f.pos))
|
||||
resp := f.c.Request(http.MethodGet, u.Url, nil,
|
||||
h.Set("Range", fmt.Sprintf("bytes=%d-", pos))
|
||||
resp := f.c.Request(http.MethodGet, urlStr, nil,
|
||||
request.WithContext(f.o.Ctx),
|
||||
request.WithLogger(f.l),
|
||||
request.WithHeader(h),
|
||||
).CheckHTTPResponse(http.StatusOK, http.StatusPartialContent)
|
||||
if resp.Err != nil {
|
||||
return fmt.Errorf("failed to request download url: %w", resp.Err)
|
||||
return nil, fmt.Errorf("failed to request download url: %w", resp.Err)
|
||||
}
|
||||
|
||||
f.rsc = resp.Response.Body
|
||||
return nil
|
||||
return resp.Response.Body, nil
|
||||
}
|
||||
|
||||
// capExpireTime make sure expire time is not too long or too short (if min or max is set)
|
||||
|
||||
@@ -85,7 +85,10 @@ type (
|
||||
}
|
||||
|
||||
Archiver interface {
|
||||
// CreateArchive creates an archive
|
||||
CreateArchive(ctx context.Context, uris []*fs.URI, writer io.Writer, opts ...fs.Option) (int, error)
|
||||
// ListArchiveFiles lists files in an archive
|
||||
ListArchiveFiles(ctx context.Context, uri *fs.URI, entity, zipEncoding string) ([]ArchivedFile, error)
|
||||
}
|
||||
|
||||
FileManager interface {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/mediameta"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/queue"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/samber/lo"
|
||||
@@ -106,6 +107,11 @@ func (m *manager) ExtractAndSaveMediaMeta(ctx context.Context, uri *fs.URI, enti
|
||||
return nil
|
||||
}
|
||||
|
||||
language := ""
|
||||
if file.Owner().Settings != nil {
|
||||
language = file.Owner().Settings.Language
|
||||
}
|
||||
|
||||
var (
|
||||
metas []driver.MediaMeta
|
||||
)
|
||||
@@ -117,7 +123,7 @@ func (m *manager) ExtractAndSaveMediaMeta(ctx context.Context, uri *fs.URI, enti
|
||||
driverCaps := d.Capabilities()
|
||||
if util.IsInExtensionList(driverCaps.MediaMetaSupportedExts, file.Name()) {
|
||||
m.l.Debug("Using native driver to generate media meta.")
|
||||
metas, err = d.MediaMeta(ctx, targetVersion.Source(), file.Ext())
|
||||
metas, err = d.MediaMeta(ctx, targetVersion.Source(), file.Ext(), language)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get media meta using native driver: %w", err)
|
||||
}
|
||||
@@ -130,7 +136,7 @@ func (m *manager) ExtractAndSaveMediaMeta(ctx context.Context, uri *fs.URI, enti
|
||||
return fmt.Errorf("failed to get entity source: %w", err)
|
||||
}
|
||||
|
||||
metas, err = extractor.Extract(ctx, file.Ext(), source)
|
||||
metas, err = extractor.Extract(ctx, file.Ext(), source, mediameta.WithLanguage(language))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to extract media meta using local extractor: %w", err)
|
||||
}
|
||||
|
||||
@@ -97,6 +97,15 @@ var (
|
||||
},
|
||||
},
|
||||
"dav": {},
|
||||
// Allow manipulating thumbnail metadata via public PatchMetadata API
|
||||
"thumb": {
|
||||
// Only supported thumb metadata currently is thumb:disabled
|
||||
dbfs.ThumbDisabledKey: func(ctx context.Context, m *manager, patch *fs.MetadataPatch) error {
|
||||
// Presence of this key disables thumbnails; value is ignored.
|
||||
// We allow both setting and removing this key.
|
||||
return nil
|
||||
},
|
||||
},
|
||||
customizeMetadataSuffix: {
|
||||
iconColorMetadataKey: validateColor(false),
|
||||
emojiIconMetadataKey: func(ctx context.Context, m *manager, patch *fs.MetadataPatch) error {
|
||||
|
||||
@@ -182,14 +182,9 @@ func (m *manager) generateThumb(ctx context.Context, uri *fs.URI, ext string, es
|
||||
entityType := types.EntityTypeThumbnail
|
||||
req := &fs.UploadRequest{
|
||||
Props: &fs.UploadProps{
|
||||
Uri: uri,
|
||||
Size: fileInfo.Size(),
|
||||
SavePath: fmt.Sprintf(
|
||||
"%s.%s%s",
|
||||
es.Entity().Source(),
|
||||
util.RandStringRunes(16),
|
||||
m.settings.ThumbEntitySuffix(ctx),
|
||||
),
|
||||
Uri: uri,
|
||||
Size: fileInfo.Size(),
|
||||
SavePath: es.Entity().Source() + m.settings.ThumbEntitySuffix(ctx),
|
||||
MimeType: m.dep.MimeDetector(ctx).TypeByName("thumb.jpg"),
|
||||
EntityType: &entityType,
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/queue"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/mholt/archiver/v4"
|
||||
"github.com/mholt/archives"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -40,13 +40,15 @@ type (
|
||||
}
|
||||
ExtractArchiveTaskPhase string
|
||||
ExtractArchiveTaskState struct {
|
||||
Uri string `json:"uri,omitempty"`
|
||||
Encoding string `json:"encoding,omitempty"`
|
||||
Dst string `json:"dst,omitempty"`
|
||||
TempPath string `json:"temp_path,omitempty"`
|
||||
TempZipFilePath string `json:"temp_zip_file_path,omitempty"`
|
||||
ProcessedCursor string `json:"processed_cursor,omitempty"`
|
||||
SlaveTaskID int `json:"slave_task_id,omitempty"`
|
||||
Uri string `json:"uri,omitempty"`
|
||||
Encoding string `json:"encoding,omitempty"`
|
||||
Dst string `json:"dst,omitempty"`
|
||||
TempPath string `json:"temp_path,omitempty"`
|
||||
TempZipFilePath string `json:"temp_zip_file_path,omitempty"`
|
||||
ProcessedCursor string `json:"processed_cursor,omitempty"`
|
||||
SlaveTaskID int `json:"slave_task_id,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
FileMask []string `json:"file_mask,omitempty"`
|
||||
NodeState `json:",inline"`
|
||||
Phase ExtractArchiveTaskPhase `json:"phase,omitempty"`
|
||||
}
|
||||
@@ -71,12 +73,14 @@ func init() {
|
||||
}
|
||||
|
||||
// NewExtractArchiveTask creates a new ExtractArchiveTask
|
||||
func NewExtractArchiveTask(ctx context.Context, src, dst, encoding string) (queue.Task, error) {
|
||||
func NewExtractArchiveTask(ctx context.Context, src, dst, encoding, password string, mask []string) (queue.Task, error) {
|
||||
state := &ExtractArchiveTaskState{
|
||||
Uri: src,
|
||||
Dst: dst,
|
||||
Encoding: encoding,
|
||||
NodeState: NodeState{},
|
||||
Password: password,
|
||||
FileMask: mask,
|
||||
}
|
||||
stateBytes, err := json.Marshal(state)
|
||||
if err != nil {
|
||||
@@ -197,6 +201,8 @@ func (m *ExtractArchiveTask) createSlaveExtractTask(ctx context.Context, dep dep
|
||||
Encoding: m.state.Encoding,
|
||||
Dst: m.state.Dst,
|
||||
UserID: user.ID,
|
||||
Password: m.state.Password,
|
||||
FileMask: m.state.FileMask,
|
||||
}
|
||||
|
||||
payloadStr, err := json.Marshal(payload)
|
||||
@@ -277,20 +283,21 @@ func (m *ExtractArchiveTask) masterExtractArchive(ctx context.Context, dep depen
|
||||
|
||||
m.l.Info("Extracting archive %q to %q", uri, m.state.Dst)
|
||||
// Identify file format
|
||||
format, readStream, err := archiver.Identify(archiveFile.DisplayName(), es)
|
||||
format, readStream, err := archives.Identify(ctx, archiveFile.DisplayName(), es)
|
||||
if err != nil {
|
||||
return task.StatusError, fmt.Errorf("failed to identify archive format: %w", err)
|
||||
}
|
||||
|
||||
m.l.Info("Archive file %q format identified as %q", uri, format.Name())
|
||||
m.l.Info("Archive file %q format identified as %q", uri, format.Extension())
|
||||
|
||||
extractor, ok := format.(archiver.Extractor)
|
||||
extractor, ok := format.(archives.Extractor)
|
||||
if !ok {
|
||||
return task.StatusError, fmt.Errorf("format not an extractor %s")
|
||||
return task.StatusError, fmt.Errorf("format not an extractor %s", format.Extension())
|
||||
}
|
||||
|
||||
if format.Name() == ".zip" {
|
||||
// Zip extractor requires a Seeker+ReadAt
|
||||
formatExt := format.Extension()
|
||||
if formatExt == ".zip" || formatExt == ".7z" {
|
||||
// Zip/7Z extractor requires a Seeker+ReadAt
|
||||
if m.state.TempZipFilePath == "" && !es.IsLocal() {
|
||||
m.state.Phase = ExtractArchivePhaseDownloadZip
|
||||
m.ResumeAfter(0)
|
||||
@@ -315,11 +322,25 @@ func (m *ExtractArchiveTask) masterExtractArchive(ctx context.Context, dep depen
|
||||
|
||||
readStream = es
|
||||
}
|
||||
}
|
||||
|
||||
if zipExtractor, ok := extractor.(archives.Zip); ok {
|
||||
if m.state.Encoding != "" {
|
||||
m.l.Info("Using encoding %q for zip archive", m.state.Encoding)
|
||||
extractor = archiver.Zip{TextEncoding: m.state.Encoding}
|
||||
encoding, ok := manager.ZipEncodings[strings.ToLower(m.state.Encoding)]
|
||||
if !ok {
|
||||
m.l.Warning("Unknown encoding %q, fallback to default encoding", m.state.Encoding)
|
||||
} else {
|
||||
zipExtractor.TextEncoding = encoding
|
||||
extractor = zipExtractor
|
||||
}
|
||||
}
|
||||
} else if rarExtractor, ok := extractor.(archives.Rar); ok && m.state.Password != "" {
|
||||
rarExtractor.Password = m.state.Password
|
||||
extractor = rarExtractor
|
||||
} else if sevenZipExtractor, ok := extractor.(archives.SevenZip); ok && m.state.Password != "" {
|
||||
sevenZipExtractor.Password = m.state.Password
|
||||
extractor = sevenZipExtractor
|
||||
}
|
||||
|
||||
needSkipToCursor := false
|
||||
@@ -332,7 +353,7 @@ func (m *ExtractArchiveTask) masterExtractArchive(ctx context.Context, dep depen
|
||||
m.Unlock()
|
||||
|
||||
// extract and upload
|
||||
err = extractor.Extract(ctx, readStream, nil, func(ctx context.Context, f archiver.File) error {
|
||||
err = extractor.Extract(ctx, readStream, func(ctx context.Context, f archives.FileInfo) error {
|
||||
if needSkipToCursor && f.NameInArchive != m.state.ProcessedCursor {
|
||||
atomic.AddInt64(&m.progress[ProgressTypeExtractCount].Current, 1)
|
||||
atomic.AddInt64(&m.progress[ProgressTypeExtractSize].Current, f.Size())
|
||||
@@ -351,6 +372,14 @@ func (m *ExtractArchiveTask) masterExtractArchive(ctx context.Context, dep depen
|
||||
rawPath := util.FormSlash(f.NameInArchive)
|
||||
savePath := dst.JoinRaw(rawPath)
|
||||
|
||||
// If file mask is not empty, check if the path is in the mask
|
||||
if len(m.state.FileMask) > 0 && !isFileInMask(rawPath, m.state.FileMask) {
|
||||
m.l.Warning("File %q is not in the mask, skipping...", f.NameInArchive)
|
||||
atomic.AddInt64(&m.progress[ProgressTypeExtractCount].Current, 1)
|
||||
atomic.AddInt64(&m.progress[ProgressTypeExtractSize].Current, f.Size())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if path is legit
|
||||
if !strings.HasPrefix(savePath.Path(), util.FillSlash(path.Clean(dst.Path()))) {
|
||||
m.l.Warning("Path %q is not legit, skipping...", f.NameInArchive)
|
||||
@@ -380,6 +409,10 @@ func (m *ExtractArchiveTask) masterExtractArchive(ctx context.Context, dep depen
|
||||
Props: &fs.UploadProps{
|
||||
Uri: savePath,
|
||||
Size: f.Size(),
|
||||
LastModified: func() *time.Time {
|
||||
t := f.FileInfo.ModTime().Local()
|
||||
return &t
|
||||
}(),
|
||||
},
|
||||
ProgressFunc: func(current, diff int64, total int64) {
|
||||
atomic.AddInt64(&m.progress[ProgressTypeExtractSize].Current, diff)
|
||||
@@ -533,6 +566,8 @@ type (
|
||||
TempPath string `json:"temp_path,omitempty"`
|
||||
TempZipFilePath string `json:"temp_zip_file_path,omitempty"`
|
||||
ProcessedCursor string `json:"processed_cursor,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
FileMask []string `json:"file_mask,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -602,18 +637,19 @@ func (m *SlaveExtractArchiveTask) Do(ctx context.Context) (task.Status, error) {
|
||||
defer es.Close()
|
||||
|
||||
// 2. Identify file format
|
||||
format, readStream, err := archiver.Identify(m.state.FileName, es)
|
||||
format, readStream, err := archives.Identify(ctx, m.state.FileName, es)
|
||||
if err != nil {
|
||||
return task.StatusError, fmt.Errorf("failed to identify archive format: %w", err)
|
||||
}
|
||||
m.l.Info("Archive file %q format identified as %q", m.state.FileName, format.Name())
|
||||
m.l.Info("Archive file %q format identified as %q", m.state.FileName, format.Extension())
|
||||
|
||||
extractor, ok := format.(archiver.Extractor)
|
||||
extractor, ok := format.(archives.Extractor)
|
||||
if !ok {
|
||||
return task.StatusError, fmt.Errorf("format not an extractor %s")
|
||||
return task.StatusError, fmt.Errorf("format not an extractor %q", format.Extension())
|
||||
}
|
||||
|
||||
if format.Name() == ".zip" {
|
||||
formatExt := format.Extension()
|
||||
if formatExt == ".zip" || formatExt == ".7z" {
|
||||
if _, err = es.Seek(0, 0); err != nil {
|
||||
return task.StatusError, fmt.Errorf("failed to seek entity source: %w", err)
|
||||
}
|
||||
@@ -666,11 +702,25 @@ func (m *SlaveExtractArchiveTask) Do(ctx context.Context) (task.Status, error) {
|
||||
if es.IsLocal() {
|
||||
readStream = es
|
||||
}
|
||||
}
|
||||
|
||||
if zipExtractor, ok := extractor.(archives.Zip); ok {
|
||||
if m.state.Encoding != "" {
|
||||
m.l.Info("Using encoding %q for zip archive", m.state.Encoding)
|
||||
extractor = archiver.Zip{TextEncoding: m.state.Encoding}
|
||||
encoding, ok := manager.ZipEncodings[strings.ToLower(m.state.Encoding)]
|
||||
if !ok {
|
||||
m.l.Warning("Unknown encoding %q, fallback to default encoding", m.state.Encoding)
|
||||
} else {
|
||||
zipExtractor.TextEncoding = encoding
|
||||
extractor = zipExtractor
|
||||
}
|
||||
}
|
||||
} else if rarExtractor, ok := extractor.(archives.Rar); ok && m.state.Password != "" {
|
||||
rarExtractor.Password = m.state.Password
|
||||
extractor = rarExtractor
|
||||
} else if sevenZipExtractor, ok := extractor.(archives.SevenZip); ok && m.state.Password != "" {
|
||||
sevenZipExtractor.Password = m.state.Password
|
||||
extractor = sevenZipExtractor
|
||||
}
|
||||
|
||||
needSkipToCursor := false
|
||||
@@ -679,7 +729,7 @@ func (m *SlaveExtractArchiveTask) Do(ctx context.Context) (task.Status, error) {
|
||||
}
|
||||
|
||||
// 3. Extract and upload
|
||||
err = extractor.Extract(ctx, readStream, nil, func(ctx context.Context, f archiver.File) error {
|
||||
err = extractor.Extract(ctx, readStream, func(ctx context.Context, f archives.FileInfo) error {
|
||||
if needSkipToCursor && f.NameInArchive != m.state.ProcessedCursor {
|
||||
atomic.AddInt64(&m.progress[ProgressTypeExtractCount].Current, 1)
|
||||
atomic.AddInt64(&m.progress[ProgressTypeExtractSize].Current, f.Size())
|
||||
@@ -698,6 +748,12 @@ func (m *SlaveExtractArchiveTask) Do(ctx context.Context) (task.Status, error) {
|
||||
rawPath := util.FormSlash(f.NameInArchive)
|
||||
savePath := dst.JoinRaw(rawPath)
|
||||
|
||||
// If file mask is not empty, check if the path is in the mask
|
||||
if len(m.state.FileMask) > 0 && !isFileInMask(rawPath, m.state.FileMask) {
|
||||
m.l.Debug("File %q is not in the mask, skipping...", f.NameInArchive)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if path is legit
|
||||
if !strings.HasPrefix(savePath.Path(), util.FillSlash(path.Clean(dst.Path()))) {
|
||||
atomic.AddInt64(&m.progress[ProgressTypeExtractCount].Current, 1)
|
||||
@@ -727,6 +783,10 @@ func (m *SlaveExtractArchiveTask) Do(ctx context.Context) (task.Status, error) {
|
||||
Props: &fs.UploadProps{
|
||||
Uri: savePath,
|
||||
Size: f.Size(),
|
||||
LastModified: func() *time.Time {
|
||||
t := f.FileInfo.ModTime().Local()
|
||||
return &t
|
||||
}(),
|
||||
},
|
||||
ProgressFunc: func(current, diff int64, total int64) {
|
||||
atomic.AddInt64(&m.progress[ProgressTypeExtractSize].Current, diff)
|
||||
@@ -765,3 +825,17 @@ func (m *SlaveExtractArchiveTask) Progress(ctx context.Context) queue.Progresses
|
||||
defer m.Unlock()
|
||||
return m.progress
|
||||
}
|
||||
|
||||
func isFileInMask(path string, mask []string) bool {
|
||||
if len(mask) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, m := range mask {
|
||||
if path == m || strings.HasPrefix(path, m+"/") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -145,7 +145,12 @@ func (e *exifExtractor) Exts() []string {
|
||||
}
|
||||
|
||||
// Reference: https://github.com/photoprism/photoprism/blob/602097635f1c84d91f2d919f7aedaef7a07fc458/internal/meta/exif.go
|
||||
func (e *exifExtractor) Extract(ctx context.Context, ext string, source entitysource.EntitySource) ([]driver.MediaMeta, error) {
|
||||
func (e *exifExtractor) Extract(ctx context.Context, ext string, source entitysource.EntitySource, opts ...optionFunc) ([]driver.MediaMeta, error) {
|
||||
option := &option{}
|
||||
for _, opt := range opts {
|
||||
opt.apply(option)
|
||||
}
|
||||
|
||||
localLimit, remoteLimit := e.settings.MediaMetaExifSizeLimit(ctx)
|
||||
if err := checkFileSize(localLimit, remoteLimit, source); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -4,12 +4,14 @@ import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
"github.com/samber/lo"
|
||||
"io"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -17,7 +19,7 @@ type (
|
||||
// Exts returns the supported file extensions.
|
||||
Exts() []string
|
||||
// Extract extracts the media meta from the given source.
|
||||
Extract(ctx context.Context, ext string, source entitysource.EntitySource) ([]driver.MediaMeta, error)
|
||||
Extract(ctx context.Context, ext string, source entitysource.EntitySource, opts ...optionFunc) ([]driver.MediaMeta, error)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -29,7 +31,7 @@ func init() {
|
||||
gob.Register([]driver.MediaMeta{})
|
||||
}
|
||||
|
||||
func NewExtractorManager(ctx context.Context, settings setting.Provider, l logging.Logger) Extractor {
|
||||
func NewExtractorManager(ctx context.Context, settings setting.Provider, l logging.Logger, client request.Client) Extractor {
|
||||
e := &extractorManager{
|
||||
settings: settings,
|
||||
extMap: make(map[string][]Extractor),
|
||||
@@ -52,6 +54,11 @@ func NewExtractorManager(ctx context.Context, settings setting.Provider, l loggi
|
||||
extractors = append(extractors, ffprobeE)
|
||||
}
|
||||
|
||||
if e.settings.MediaMetaGeocodingEnabled(ctx) {
|
||||
geocodingE := newGeocodingExtractor(settings, l, client)
|
||||
extractors = append(extractors, geocodingE)
|
||||
}
|
||||
|
||||
for _, extractor := range extractors {
|
||||
for _, ext := range extractor.Exts() {
|
||||
if e.extMap[ext] == nil {
|
||||
@@ -73,12 +80,12 @@ func (e *extractorManager) Exts() []string {
|
||||
return lo.Keys(e.extMap)
|
||||
}
|
||||
|
||||
func (e *extractorManager) Extract(ctx context.Context, ext string, source entitysource.EntitySource) ([]driver.MediaMeta, error) {
|
||||
func (e *extractorManager) Extract(ctx context.Context, ext string, source entitysource.EntitySource, opts ...optionFunc) ([]driver.MediaMeta, error) {
|
||||
if extractor, ok := e.extMap[ext]; ok {
|
||||
res := []driver.MediaMeta{}
|
||||
for _, e := range extractor {
|
||||
_, _ = source.Seek(0, io.SeekStart)
|
||||
data, err := e.Extract(ctx, ext, source)
|
||||
data, err := e.Extract(ctx, ext, source, append(opts, WithExtracted(res))...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -92,6 +99,29 @@ func (e *extractorManager) Extract(ctx context.Context, ext string, source entit
|
||||
}
|
||||
}
|
||||
|
||||
type option struct {
|
||||
extracted []driver.MediaMeta
|
||||
language string
|
||||
}
|
||||
|
||||
type optionFunc func(*option)
|
||||
|
||||
func (f optionFunc) apply(o *option) {
|
||||
f(o)
|
||||
}
|
||||
|
||||
func WithExtracted(extracted []driver.MediaMeta) optionFunc {
|
||||
return optionFunc(func(o *option) {
|
||||
o.extracted = extracted
|
||||
})
|
||||
}
|
||||
|
||||
func WithLanguage(language string) optionFunc {
|
||||
return optionFunc(func(o *option) {
|
||||
o.language = language
|
||||
})
|
||||
}
|
||||
|
||||
// checkFileSize checks if the file size exceeds the limit.
|
||||
func checkFileSize(localLimit, remoteLimit int64, source entitysource.EntitySource) error {
|
||||
if source.IsLocal() && localLimit > 0 && source.Entity().Size() > localLimit {
|
||||
|
||||
@@ -88,7 +88,12 @@ func (f *ffprobeExtractor) Exts() []string {
|
||||
return ffprobeExts
|
||||
}
|
||||
|
||||
func (f *ffprobeExtractor) Extract(ctx context.Context, ext string, source entitysource.EntitySource) ([]driver.MediaMeta, error) {
|
||||
func (f *ffprobeExtractor) Extract(ctx context.Context, ext string, source entitysource.EntitySource, opts ...optionFunc) ([]driver.MediaMeta, error) {
|
||||
option := &option{}
|
||||
for _, opt := range opts {
|
||||
opt.apply(option)
|
||||
}
|
||||
|
||||
localLimit, remoteLimit := f.settings.MediaMetaFFProbeSizeLimit(ctx)
|
||||
if err := checkFileSize(localLimit, remoteLimit, source); err != nil {
|
||||
return nil, err
|
||||
|
||||
236
pkg/mediameta/geocoding.go
Normal file
236
pkg/mediameta/geocoding.go
Normal file
@@ -0,0 +1,236 @@
|
||||
package mediameta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
)
|
||||
|
||||
const mapBoxURL = "https://api.mapbox.com/search/geocode/v6/reverse"
|
||||
|
||||
const (
|
||||
Street = "street"
|
||||
Locality = "locality"
|
||||
Place = "place"
|
||||
District = "district"
|
||||
Region = "region"
|
||||
Country = "country"
|
||||
)
|
||||
|
||||
type geocodingExtractor struct {
|
||||
settings setting.Provider
|
||||
l logging.Logger
|
||||
client request.Client
|
||||
}
|
||||
|
||||
func newGeocodingExtractor(settings setting.Provider, l logging.Logger, client request.Client) *geocodingExtractor {
|
||||
return &geocodingExtractor{
|
||||
settings: settings,
|
||||
l: l,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *geocodingExtractor) Exts() []string {
|
||||
return exifExts
|
||||
}
|
||||
|
||||
func (e *geocodingExtractor) Extract(ctx context.Context, ext string, source entitysource.EntitySource, opts ...optionFunc) ([]driver.MediaMeta, error) {
|
||||
option := &option{}
|
||||
for _, opt := range opts {
|
||||
opt.apply(option)
|
||||
}
|
||||
|
||||
// Find GPS info from extracted
|
||||
var latStr, lngStr string
|
||||
for _, meta := range option.extracted {
|
||||
if meta.Key == GpsLat {
|
||||
latStr = meta.Value
|
||||
}
|
||||
if meta.Key == GpsLng {
|
||||
lngStr = meta.Value
|
||||
}
|
||||
}
|
||||
|
||||
if latStr == "" || lngStr == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
lat, err := strconv.ParseFloat(latStr, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("geocoding: failed to parse latitude: %w", err)
|
||||
}
|
||||
|
||||
lng, err := strconv.ParseFloat(lngStr, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("geocoding: failed to parse longitude: %w", err)
|
||||
}
|
||||
|
||||
metas, err := e.getGeocoding(ctx, lat, lng, option.language)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("geocoding: failed to get geocoding: %w", err)
|
||||
}
|
||||
|
||||
for i, _ := range metas {
|
||||
metas[i].Type = driver.MetaTypeGeocoding
|
||||
}
|
||||
|
||||
return metas, nil
|
||||
}
|
||||
|
||||
func (e *geocodingExtractor) getGeocoding(ctx context.Context, lat, lng float64, language string) ([]driver.MediaMeta, error) {
|
||||
values := url.Values{}
|
||||
values.Add("longitude", fmt.Sprintf("%f", lng))
|
||||
values.Add("latitude", fmt.Sprintf("%f", lat))
|
||||
values.Add("limit", "1")
|
||||
values.Add("access_token", e.settings.MediaMetaGeocodingMapboxAK(ctx))
|
||||
if language != "" {
|
||||
values.Add("language", language)
|
||||
}
|
||||
|
||||
resp, err := e.client.Request(
|
||||
"GET",
|
||||
mapBoxURL+"?"+values.Encode(),
|
||||
nil,
|
||||
request.WithContext(ctx),
|
||||
request.WithLogger(e.l),
|
||||
).CheckHTTPResponse(http.StatusOK).GetResponse()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get geocoding from mapbox: %w", err)
|
||||
}
|
||||
|
||||
var geocoding MapboxGeocodingResponse
|
||||
if err := json.Unmarshal([]byte(resp), &geocoding); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal geocoding from mapbox: %w", err)
|
||||
}
|
||||
|
||||
if len(geocoding.Features) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
metas := make([]driver.MediaMeta, 0)
|
||||
contexts := geocoding.Features[0].Properties.Context
|
||||
if contexts.Street != nil {
|
||||
metas = append(metas, driver.MediaMeta{
|
||||
Key: Street,
|
||||
Value: contexts.Street.Name,
|
||||
})
|
||||
}
|
||||
if contexts.Locality != nil {
|
||||
metas = append(metas, driver.MediaMeta{
|
||||
Key: Locality,
|
||||
Value: contexts.Locality.Name,
|
||||
})
|
||||
}
|
||||
if contexts.Place != nil {
|
||||
metas = append(metas, driver.MediaMeta{
|
||||
Key: Place,
|
||||
Value: contexts.Place.Name,
|
||||
})
|
||||
}
|
||||
if contexts.District != nil {
|
||||
metas = append(metas, driver.MediaMeta{
|
||||
Key: District,
|
||||
Value: contexts.District.Name,
|
||||
})
|
||||
}
|
||||
if contexts.Region != nil {
|
||||
metas = append(metas, driver.MediaMeta{
|
||||
Key: Region,
|
||||
Value: contexts.Region.Name,
|
||||
})
|
||||
}
|
||||
if contexts.Country != nil {
|
||||
metas = append(metas, driver.MediaMeta{
|
||||
Key: Country,
|
||||
Value: contexts.Country.Name,
|
||||
})
|
||||
}
|
||||
|
||||
return metas, nil
|
||||
}
|
||||
|
||||
// MapboxGeocodingResponse represents the response from Mapbox Geocoding API
|
||||
type MapboxGeocodingResponse struct {
|
||||
Type string `json:"type"` // "FeatureCollection"
|
||||
Features []Feature `json:"features"` // Array of feature objects
|
||||
Attribution string `json:"attribution"` // Attribution to Mapbox
|
||||
}
|
||||
|
||||
// Feature represents a feature object in the geocoding response
|
||||
type Feature struct {
|
||||
ID string `json:"id"` // Feature ID (same as mapbox_id)
|
||||
Type string `json:"type"` // "Feature"
|
||||
Geometry Geometry `json:"geometry"` // Spatial geometry of the feature
|
||||
Properties Properties `json:"properties"` // Feature details
|
||||
}
|
||||
|
||||
// Geometry represents the spatial geometry of a feature
|
||||
type Geometry struct {
|
||||
Type string `json:"type"` // "Point"
|
||||
Coordinates []float64 `json:"coordinates"` // [longitude, latitude]
|
||||
}
|
||||
|
||||
// Properties contains the feature's detailed information
|
||||
type Properties struct {
|
||||
MapboxID string `json:"mapbox_id"` // Unique feature identifier
|
||||
FeatureType string `json:"feature_type"` // Type of feature (country, region, etc.)
|
||||
Name string `json:"name"` // Formatted address string
|
||||
NamePreferred string `json:"name_preferred"` // Canonical or common alias
|
||||
PlaceFormatted string `json:"place_formatted"` // Formatted context string
|
||||
FullAddress string `json:"full_address"` // Full formatted address
|
||||
Context Context `json:"context"` // Hierarchy of parent features
|
||||
Coordinates Coordinates `json:"coordinates"` // Geographic position and accuracy
|
||||
BBox []float64 `json:"bbox,omitempty"` // Bounding box [minLon,minLat,maxLon,maxLat]
|
||||
MatchCode MatchCode `json:"match_code"` // Metadata about result matching
|
||||
}
|
||||
|
||||
// Context represents the hierarchy of encompassing parent features
|
||||
type Context struct {
|
||||
Country *ContextFeature `json:"country,omitempty"`
|
||||
Region *ContextFeature `json:"region,omitempty"`
|
||||
Postcode *ContextFeature `json:"postcode,omitempty"`
|
||||
District *ContextFeature `json:"district,omitempty"`
|
||||
Place *ContextFeature `json:"place,omitempty"`
|
||||
Locality *ContextFeature `json:"locality,omitempty"`
|
||||
Neighborhood *ContextFeature `json:"neighborhood,omitempty"`
|
||||
Street *ContextFeature `json:"street,omitempty"`
|
||||
}
|
||||
|
||||
// ContextFeature represents a feature in the context hierarchy
|
||||
type ContextFeature struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
NamePreferred string `json:"name_preferred,omitempty"`
|
||||
MapboxID string `json:"mapbox_id"`
|
||||
}
|
||||
|
||||
// Coordinates represents geographical position and accuracy information
|
||||
type Coordinates struct {
|
||||
Longitude float64 `json:"longitude"` // Longitude of result
|
||||
Latitude float64 `json:"latitude"` // Latitude of result
|
||||
Accuracy string `json:"accuracy,omitempty"` // Accuracy metric for address results
|
||||
RoutablePoints []RoutablePoint `json:"routable_points,omitempty"` // Array of routable points
|
||||
}
|
||||
|
||||
// RoutablePoint represents a routable point for an address feature
|
||||
type RoutablePoint struct {
|
||||
Name string `json:"name"` // Name of the routable point
|
||||
Longitude float64 `json:"longitude"` // Longitude coordinate
|
||||
Latitude float64 `json:"latitude"` // Latitude coordinate
|
||||
}
|
||||
|
||||
// MatchCode contains metadata about how result components match the input query
|
||||
type MatchCode struct {
|
||||
// Add specific match code fields as needed based on Mapbox documentation
|
||||
// This structure may vary depending on the specific match codes returned
|
||||
}
|
||||
@@ -48,7 +48,12 @@ func (a *musicExtractor) Exts() []string {
|
||||
return audioExts
|
||||
}
|
||||
|
||||
func (a *musicExtractor) Extract(ctx context.Context, ext string, source entitysource.EntitySource) ([]driver.MediaMeta, error) {
|
||||
func (a *musicExtractor) Extract(ctx context.Context, ext string, source entitysource.EntitySource, opts ...optionFunc) ([]driver.MediaMeta, error) {
|
||||
option := &option{}
|
||||
for _, opt := range opts {
|
||||
opt.apply(option)
|
||||
}
|
||||
|
||||
localLimit, remoteLimit := a.settings.MediaMetaMusicSizeLimit(ctx)
|
||||
if err := checkFileSize(localLimit, remoteLimit, source); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -253,6 +253,8 @@ const (
|
||||
CodeNodeUsedByStoragePolicy = 40086
|
||||
// CodeDomainNotLicensed domain not licensed
|
||||
CodeDomainNotLicensed = 40087
|
||||
// CodeAnonymouseAccessDenied 匿名用户无法访问分享
|
||||
CodeAnonymouseAccessDenied = 40088
|
||||
// CodeDBError 数据库操作失败
|
||||
CodeDBError = 50001
|
||||
// CodeEncryptError 加密失败
|
||||
|
||||
@@ -102,6 +102,10 @@ type (
|
||||
MediaMetaFFProbeSizeLimit(ctx context.Context) (int64, int64)
|
||||
// MediaMetaFFProbePath returns the path of ffprobe executable.
|
||||
MediaMetaFFProbePath(ctx context.Context) string
|
||||
// MediaMetaGeocodingEnabled returns true if media meta geocoding is enabled.
|
||||
MediaMetaGeocodingEnabled(ctx context.Context) bool
|
||||
// MediaMetaGeocodingMapboxAK returns the Mapbox access token.
|
||||
MediaMetaGeocodingMapboxAK(ctx context.Context) string
|
||||
// ThumbSize returns the size limit of thumbnails.
|
||||
ThumbSize(ctx context.Context) (int, int)
|
||||
// ThumbEncode returns the thumbnail encoding settings.
|
||||
@@ -306,6 +310,7 @@ func (s *settingProvider) MapSetting(ctx context.Context) *MapSetting {
|
||||
return &MapSetting{
|
||||
Provider: MapProvider(s.getString(ctx, "map_provider", "openstreetmap")),
|
||||
GoogleTileType: MapGoogleTileType(s.getString(ctx, "map_google_tile_type", "roadmap")),
|
||||
MapboxAK: s.getString(ctx, "map_mapbox_ak", ""),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,6 +532,14 @@ func (s *settingProvider) MediaMetaEnabled(ctx context.Context) bool {
|
||||
return s.getBoolean(ctx, "media_meta", true)
|
||||
}
|
||||
|
||||
func (s *settingProvider) MediaMetaGeocodingEnabled(ctx context.Context) bool {
|
||||
return s.getBoolean(ctx, "media_meta_geocoding", false)
|
||||
}
|
||||
|
||||
func (s *settingProvider) MediaMetaGeocodingMapboxAK(ctx context.Context) string {
|
||||
return s.getString(ctx, "media_meta_geocoding_mapbox_ak", "")
|
||||
}
|
||||
|
||||
func (s *settingProvider) PublicResourceMaxAge(ctx context.Context) int {
|
||||
return s.getInt(ctx, "public_resource_maxage", 0)
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@ type MapProvider string
|
||||
const (
|
||||
MapProviderOpenStreetMap = MapProvider("openstreetmap")
|
||||
MapProviderGoogle = MapProvider("google")
|
||||
MapProviderMapbox = MapProvider("mapbox")
|
||||
)
|
||||
|
||||
type MapGoogleTileType string
|
||||
@@ -173,6 +174,7 @@ const (
|
||||
type MapSetting struct {
|
||||
Provider MapProvider
|
||||
GoogleTileType MapGoogleTileType
|
||||
MapboxAK string
|
||||
}
|
||||
|
||||
// Viewer related
|
||||
|
||||
@@ -19,6 +19,10 @@ import (
|
||||
|
||||
const thumbTempFolder = "thumb"
|
||||
|
||||
// BuiltinSupportedExts lists file extensions supported by the built-in
|
||||
// thumbnail generator. Extensions are lowercased and do not include the dot.
|
||||
var BuiltinSupportedExts = []string{"jpg", "jpeg", "png", "gif"}
|
||||
|
||||
// Thumb 缩略图
|
||||
type Thumb struct {
|
||||
src image.Image
|
||||
|
||||
@@ -9,6 +9,12 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
@@ -26,11 +32,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/tools/container/intsets"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -228,6 +229,12 @@ func handlePut(c *gin.Context, user *ent.User, fm manager.FileManager) (status i
|
||||
return purposeStatusCodeFromError(err), err
|
||||
}
|
||||
|
||||
if user.Edges.DavAccounts[0].Options.Enabled(int(types.DavAccountDisableSysFiles)) {
|
||||
if strings.HasPrefix(reqPath.Name(), ".") {
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
}
|
||||
|
||||
release, ls, status, err := confirmLock(c, fm, user, ancestor, nil, uri, nil)
|
||||
if err != nil {
|
||||
return status, err
|
||||
|
||||
@@ -412,3 +412,17 @@ func PatchView(c *gin.Context) {
|
||||
|
||||
c.JSON(200, serializer.Response{})
|
||||
}
|
||||
|
||||
func ListArchiveFiles(c *gin.Context) {
|
||||
service := ParametersFromContext[*explorer.ArchiveListFilesService](c, explorer.ArchiveListFilesParamCtx{})
|
||||
resp, err := service.List(c)
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.Err(c, err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, serializer.Response{
|
||||
Data: resp,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -566,6 +566,10 @@ func initMasterRouter(dep dependency.Dep) *gin.Engine {
|
||||
controllers.FromQuery[explorer.ListFileService](explorer.ListFileParameterCtx{}),
|
||||
controllers.ListDirectory,
|
||||
)
|
||||
file.GET("archive",
|
||||
controllers.FromQuery[explorer.ArchiveListFilesService](explorer.ArchiveListFilesParamCtx{}),
|
||||
controllers.ListArchiveFiles,
|
||||
)
|
||||
// Create file
|
||||
file.POST("create",
|
||||
controllers.FromJSON[explorer.CreateFileService](explorer.CreateFileParameterCtx{}),
|
||||
@@ -614,7 +618,7 @@ func initMasterRouter(dep dependency.Dep) *gin.Engine {
|
||||
controllers.ServeEntity,
|
||||
)
|
||||
}
|
||||
// 获取缩略图
|
||||
// get thumb
|
||||
file.GET("thumb",
|
||||
middleware.ContextHint(),
|
||||
controllers.FromQuery[explorer.FileThumbService](explorer.FileThumbParameterCtx{}),
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
@@ -150,9 +151,12 @@ func (service *FileBatchService) Delete(c *gin.Context) serializer.Response {
|
||||
}
|
||||
|
||||
const (
|
||||
fileNameCondition = "file_name"
|
||||
fileUserCondition = "file_user"
|
||||
filePolicyCondition = "file_policy"
|
||||
fileNameCondition = "file_name"
|
||||
fileUserCondition = "file_user"
|
||||
filePolicyCondition = "file_policy"
|
||||
fileMetadataCondition = "file_metadata"
|
||||
fileSharedCondition = "file_shared"
|
||||
fileDirectLinkCondition = "file_direct_link"
|
||||
)
|
||||
|
||||
func (service *AdminListService) Files(c *gin.Context) (*ListFileResponse, error) {
|
||||
@@ -167,9 +171,12 @@ func (service *AdminListService) Files(c *gin.Context) (*ListFileResponse, error
|
||||
ctx = context.WithValue(ctx, inventory.LoadFileDirectLink{}, true)
|
||||
|
||||
var (
|
||||
err error
|
||||
userID int
|
||||
policyID int
|
||||
err error
|
||||
userID int
|
||||
policyID int
|
||||
metadata string
|
||||
shared bool
|
||||
directLink bool
|
||||
)
|
||||
|
||||
if service.Conditions[fileUserCondition] != "" {
|
||||
@@ -186,6 +193,18 @@ func (service *AdminListService) Files(c *gin.Context) (*ListFileResponse, error
|
||||
}
|
||||
}
|
||||
|
||||
if service.Conditions[fileMetadataCondition] != "" {
|
||||
metadata = service.Conditions[fileMetadataCondition]
|
||||
}
|
||||
|
||||
if service.Conditions[fileSharedCondition] != "" && setting.IsTrueValue(service.Conditions[fileSharedCondition]) {
|
||||
shared = true
|
||||
}
|
||||
|
||||
if service.Conditions[fileDirectLinkCondition] != "" && setting.IsTrueValue(service.Conditions[fileDirectLinkCondition]) {
|
||||
directLink = true
|
||||
}
|
||||
|
||||
res, err := fileClient.FlattenListFiles(ctx, &inventory.FlattenListFileParameters{
|
||||
PaginationArgs: &inventory.PaginationArgs{
|
||||
Page: service.Page - 1,
|
||||
@@ -196,6 +215,9 @@ func (service *AdminListService) Files(c *gin.Context) (*ListFileResponse, error
|
||||
UserID: userID,
|
||||
StoragePolicyID: policyID,
|
||||
Name: service.Conditions[fileNameCondition],
|
||||
HasMetadata: metadata,
|
||||
Shared: shared,
|
||||
HasDirectLink: directLink,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -2,9 +2,12 @@ package admin
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/boolset"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
@@ -12,9 +15,8 @@ import (
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/wopi"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-mail/mail"
|
||||
"github.com/wneessen/go-mail"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -138,27 +140,39 @@ func (s *TestSMTPService) Test(c *gin.Context) error {
|
||||
return serializer.NewError(serializer.CodeParamErr, "Invalid SMTP port", err)
|
||||
}
|
||||
|
||||
d := mail.NewDialer(s.Settings["smtpHost"], port, s.Settings["smtpUser"], s.Settings["smtpPass"])
|
||||
d.SSL = false
|
||||
opts := []mail.Option{
|
||||
mail.WithPort(port),
|
||||
mail.WithSMTPAuth(mail.SMTPAuthAutoDiscover), mail.WithTLSPortPolicy(mail.TLSOpportunistic),
|
||||
mail.WithUsername(s.Settings["smtpUser"]), mail.WithPassword(s.Settings["smtpPass"]),
|
||||
}
|
||||
if setting.IsTrueValue(s.Settings["smtpEncryption"]) {
|
||||
d.SSL = true
|
||||
}
|
||||
d.StartTLSPolicy = mail.OpportunisticStartTLS
|
||||
|
||||
sender, err := d.Dial()
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeInternalSetting, "Failed to connect to SMTP server: "+err.Error(), err)
|
||||
opts = append(opts, mail.WithSSL())
|
||||
}
|
||||
|
||||
m := mail.NewMessage()
|
||||
m.SetHeader("From", s.Settings["fromAdress"])
|
||||
m.SetAddressHeader("Reply-To", s.Settings["replyTo"], s.Settings["fromName"])
|
||||
m.SetHeader("To", s.To)
|
||||
m.SetHeader("Subject", "Cloudreve SMTP Test")
|
||||
m.SetBody("text/plain", "This is a test email from Cloudreve.")
|
||||
d, diaErr := mail.NewClient(s.Settings["smtpHost"], opts...)
|
||||
if diaErr != nil {
|
||||
return serializer.NewError(serializer.CodeInternalSetting, "Failed to create SMTP client: "+diaErr.Error(), diaErr)
|
||||
}
|
||||
|
||||
err = mail.Send(sender, m)
|
||||
m := mail.NewMsg()
|
||||
if err := m.FromFormat(s.Settings["fromName"], s.Settings["fromAdress"]); err != nil {
|
||||
return serializer.NewError(serializer.CodeInternalSetting, "Failed to set FROM address: "+err.Error(), err)
|
||||
}
|
||||
m.ReplyToFormat(s.Settings["fromName"], s.Settings["replyTo"])
|
||||
m.To(s.To)
|
||||
m.Subject("Cloudreve SMTP Test")
|
||||
m.SetMessageID()
|
||||
m.SetBodyString(mail.TypeTextHTML, "This is a test email from Cloudreve.")
|
||||
|
||||
err = d.DialAndSendWithContext(c, m)
|
||||
if err != nil {
|
||||
// Check if this is an SMTP RESET error after successful delivery
|
||||
var sendErr *mail.SendError
|
||||
var errParsed = errors.As(err, &sendErr)
|
||||
if errParsed && sendErr.Reason == mail.ErrSMTPReset {
|
||||
return nil // Don't treat this as a delivery failure since mail was sent
|
||||
}
|
||||
|
||||
return serializer.NewError(serializer.CodeInternalSetting, "Failed to send test email: "+err.Error(), err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/thumb"
|
||||
"github.com/cloudreve/Cloudreve/v4/service/user"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mojocn/base64Captcha"
|
||||
@@ -43,12 +47,16 @@ type SiteConfig struct {
|
||||
EmojiPreset string `json:"emoji_preset,omitempty"`
|
||||
MapProvider setting.MapProvider `json:"map_provider,omitempty"`
|
||||
GoogleMapTileType setting.MapGoogleTileType `json:"google_map_tile_type,omitempty"`
|
||||
MapboxAK string `json:"mapbox_ak,omitempty"`
|
||||
FileViewers []types.ViewerGroup `json:"file_viewers,omitempty"`
|
||||
MaxBatchSize int `json:"max_batch_size,omitempty"`
|
||||
ThumbnailWidth int `json:"thumbnail_width,omitempty"`
|
||||
ThumbnailHeight int `json:"thumbnail_height,omitempty"`
|
||||
CustomProps []types.CustomProps `json:"custom_props,omitempty"`
|
||||
|
||||
// Thumbnail section
|
||||
ThumbExts []string `json:"thumb_exts,omitempty"`
|
||||
|
||||
// App settings
|
||||
AppPromotion bool `json:"app_promotion,omitempty"`
|
||||
|
||||
@@ -104,6 +112,7 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) {
|
||||
Icons: explorerSettings.Icons,
|
||||
MapProvider: mapSettings.Provider,
|
||||
GoogleMapTileType: mapSettings.GoogleTileType,
|
||||
MapboxAK: mapSettings.MapboxAK,
|
||||
ThumbnailWidth: w,
|
||||
ThumbnailHeight: h,
|
||||
CustomProps: customProps,
|
||||
@@ -118,6 +127,47 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) {
|
||||
return &SiteConfig{
|
||||
AppPromotion: appSetting.Promotion,
|
||||
}, nil
|
||||
case "thumb":
|
||||
// Return supported thumbnail extensions from enabled generators.
|
||||
exts := map[string]bool{}
|
||||
if settings.BuiltinThumbGeneratorEnabled(c) {
|
||||
for _, e := range thumb.BuiltinSupportedExts {
|
||||
exts[e] = true
|
||||
}
|
||||
}
|
||||
if settings.FFMpegThumbGeneratorEnabled(c) {
|
||||
for _, e := range settings.FFMpegThumbExts(c) {
|
||||
exts[strings.ToLower(e)] = true
|
||||
}
|
||||
}
|
||||
if settings.VipsThumbGeneratorEnabled(c) {
|
||||
for _, e := range settings.VipsThumbExts(c) {
|
||||
exts[strings.ToLower(e)] = true
|
||||
}
|
||||
}
|
||||
if settings.LibreOfficeThumbGeneratorEnabled(c) {
|
||||
for _, e := range settings.LibreOfficeThumbExts(c) {
|
||||
exts[strings.ToLower(e)] = true
|
||||
}
|
||||
}
|
||||
if settings.MusicCoverThumbGeneratorEnabled(c) {
|
||||
for _, e := range settings.MusicCoverThumbExts(c) {
|
||||
exts[strings.ToLower(e)] = true
|
||||
}
|
||||
}
|
||||
if settings.LibRawThumbGeneratorEnabled(c) {
|
||||
for _, e := range settings.LibRawThumbExts(c) {
|
||||
exts[strings.ToLower(e)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// map -> sorted slice
|
||||
result := make([]string, 0, len(exts))
|
||||
for e := range exts {
|
||||
result = append(result, e)
|
||||
}
|
||||
sort.Strings(result)
|
||||
return &SiteConfig{ThumbExts: result}, nil
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@@ -716,3 +716,34 @@ func (s *PatchViewService) Patch(c *gin.Context) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type (
|
||||
ArchiveListFilesParamCtx struct{}
|
||||
ArchiveListFilesService struct {
|
||||
Uri string `form:"uri" binding:"required"`
|
||||
Entity string `form:"entity"`
|
||||
TextEncoding string `form:"text_encoding"`
|
||||
}
|
||||
)
|
||||
|
||||
func (s *ArchiveListFilesService) List(c *gin.Context) (*ArchiveListFilesResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
m := manager.NewFileManager(dep, user)
|
||||
defer m.Recycle()
|
||||
if !user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionArchiveTask)) {
|
||||
return nil, serializer.NewError(serializer.CodeGroupNotAllowed, "Group not allowed to extract archive files", nil)
|
||||
}
|
||||
|
||||
uri, err := fs.NewUriFromString(s.Uri)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
|
||||
}
|
||||
|
||||
files, err := m.ListArchiveFiles(c, uri, s.Entity, s.TextEncoding)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list archive files: %w", err)
|
||||
}
|
||||
|
||||
return BuildArchiveListFilesResponse(files), nil
|
||||
}
|
||||
|
||||
@@ -26,6 +26,16 @@ import (
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type ArchiveListFilesResponse struct {
|
||||
Files []manager.ArchivedFile `json:"files"`
|
||||
}
|
||||
|
||||
func BuildArchiveListFilesResponse(files []manager.ArchivedFile) *ArchiveListFilesResponse {
|
||||
return &ArchiveListFilesResponse{
|
||||
Files: files,
|
||||
}
|
||||
}
|
||||
|
||||
type PutRelativeResponse struct {
|
||||
Name string
|
||||
Url string
|
||||
@@ -259,6 +269,7 @@ type StoragePolicy struct {
|
||||
Type types.PolicyType `json:"type"`
|
||||
MaxSize int64 `json:"max_size"`
|
||||
Relay bool `json:"relay,omitempty"`
|
||||
ChunkConcurrency int `json:"chunk_concurrency,omitempty"`
|
||||
}
|
||||
|
||||
type Entity struct {
|
||||
@@ -271,19 +282,20 @@ type Entity struct {
|
||||
}
|
||||
|
||||
type Share struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
RemainDownloads *int `json:"remain_downloads,omitempty"`
|
||||
Visited int `json:"visited"`
|
||||
Downloaded int `json:"downloaded,omitempty"`
|
||||
Expires *time.Time `json:"expires,omitempty"`
|
||||
Unlocked bool `json:"unlocked"`
|
||||
SourceType *types.FileType `json:"source_type,omitempty"`
|
||||
Owner user.User `json:"owner"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
Expired bool `json:"expired"`
|
||||
Url string `json:"url"`
|
||||
ShowReadMe bool `json:"show_readme,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
RemainDownloads *int `json:"remain_downloads,omitempty"`
|
||||
Visited int `json:"visited"`
|
||||
Downloaded int `json:"downloaded,omitempty"`
|
||||
Expires *time.Time `json:"expires,omitempty"`
|
||||
Unlocked bool `json:"unlocked"`
|
||||
PasswordProtected bool `json:"password_protected,omitempty"`
|
||||
SourceType *types.FileType `json:"source_type,omitempty"`
|
||||
Owner user.User `json:"owner"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
Expired bool `json:"expired"`
|
||||
Url string `json:"url"`
|
||||
ShowReadMe bool `json:"show_readme,omitempty"`
|
||||
|
||||
// Only viewable by owner
|
||||
IsPrivate bool `json:"is_private,omitempty"`
|
||||
@@ -301,15 +313,16 @@ func BuildShare(s *ent.Share, base *url.URL, hasher hashid.Encoder, requester *e
|
||||
redactLevel = user.RedactLevelUser
|
||||
}
|
||||
res := Share{
|
||||
Name: name,
|
||||
ID: hashid.EncodeShareID(hasher, s.ID),
|
||||
Unlocked: unlocked,
|
||||
Owner: user.BuildUserRedacted(owner, redactLevel, hasher),
|
||||
Expired: inventory.IsShareExpired(s) != nil || expired,
|
||||
Url: BuildShareLink(s, hasher, base),
|
||||
CreatedAt: s.CreatedAt,
|
||||
Visited: s.Views,
|
||||
SourceType: util.ToPtr(t),
|
||||
Name: name,
|
||||
ID: hashid.EncodeShareID(hasher, s.ID),
|
||||
Unlocked: unlocked,
|
||||
Owner: user.BuildUserRedacted(owner, redactLevel, hasher),
|
||||
Expired: inventory.IsShareExpired(s) != nil || expired,
|
||||
Url: BuildShareLink(s, hasher, base, unlocked),
|
||||
CreatedAt: s.CreatedAt,
|
||||
Visited: s.Views,
|
||||
SourceType: util.ToPtr(t),
|
||||
PasswordProtected: s.Password != "",
|
||||
}
|
||||
|
||||
if unlocked {
|
||||
@@ -436,9 +449,12 @@ func BuildEntity(extendedInfo *fs.FileExtendedInfo, e fs.Entity, hasher hashid.E
|
||||
}
|
||||
}
|
||||
|
||||
func BuildShareLink(s *ent.Share, hasher hashid.Encoder, base *url.URL) string {
|
||||
func BuildShareLink(s *ent.Share, hasher hashid.Encoder, base *url.URL, unlocked bool) string {
|
||||
shareId := hashid.EncodeShareID(hasher, s.ID)
|
||||
return routes.MasterShareUrl(base, shareId, s.Password).String()
|
||||
if unlocked {
|
||||
return routes.MasterShareUrl(base, shareId, s.Password).String()
|
||||
}
|
||||
return routes.MasterShareUrl(base, shareId, "").String()
|
||||
}
|
||||
|
||||
func BuildStoragePolicy(sp *ent.StoragePolicy, hasher hashid.Encoder) *StoragePolicy {
|
||||
@@ -447,11 +463,12 @@ func BuildStoragePolicy(sp *ent.StoragePolicy, hasher hashid.Encoder) *StoragePo
|
||||
}
|
||||
|
||||
res := &StoragePolicy{
|
||||
ID: hashid.EncodePolicyID(hasher, sp.ID),
|
||||
Name: sp.Name,
|
||||
Type: types.PolicyType(sp.Type),
|
||||
MaxSize: sp.MaxSize,
|
||||
Relay: sp.Settings.Relay,
|
||||
ID: hashid.EncodePolicyID(hasher, sp.ID),
|
||||
Name: sp.Name,
|
||||
Type: types.PolicyType(sp.Type),
|
||||
MaxSize: sp.MaxSize,
|
||||
Relay: sp.Settings.Relay,
|
||||
ChunkConcurrency: sp.Settings.ChunkConcurrency,
|
||||
}
|
||||
|
||||
if sp.Settings.IsFileTypeDenyList {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/mediameta"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
@@ -135,8 +136,9 @@ func (s *SlaveMetaService) MediaMeta(c *gin.Context) ([]driver.MediaMeta, error)
|
||||
}
|
||||
defer entitySource.Close()
|
||||
|
||||
language := c.Query("language")
|
||||
extractor := dep.MediaMetaExtractor(c)
|
||||
res, err := extractor.Extract(c, s.Ext, entitySource)
|
||||
res, err := extractor.Extract(c, s.Ext, entitySource, mediameta.WithLanguage(language))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract media meta: %w", err)
|
||||
}
|
||||
|
||||
@@ -173,6 +173,8 @@ type (
|
||||
Src []string `json:"src" binding:"required"`
|
||||
Dst string `json:"dst" binding:"required"`
|
||||
Encoding string `json:"encoding"`
|
||||
Password string `json:"password"`
|
||||
FileMask []string `json:"file_mask"`
|
||||
}
|
||||
CreateArchiveParamCtx struct{}
|
||||
)
|
||||
@@ -203,7 +205,7 @@ func (service *ArchiveWorkflowService) CreateExtractTask(c *gin.Context) (*TaskR
|
||||
}
|
||||
|
||||
// Create task
|
||||
t, err := workflows.NewExtractArchiveTask(c, service.Src[0], service.Dst, service.Encoding)
|
||||
t, err := workflows.NewExtractArchiveTask(c, service.Src[0], service.Dst, service.Encoding, service.Password, service.FileMask)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeCreateTaskError, "Failed to create task", err)
|
||||
}
|
||||
|
||||
@@ -86,10 +86,11 @@ func (service *ListDavAccountsService) List(c *gin.Context) (*ListDavAccountResp
|
||||
|
||||
type (
|
||||
CreateDavAccountService struct {
|
||||
Uri string `json:"uri" binding:"required"`
|
||||
Name string `json:"name" binding:"required,min=1,max=255"`
|
||||
Readonly bool `json:"readonly"`
|
||||
Proxy bool `json:"proxy"`
|
||||
Uri string `json:"uri" binding:"required"`
|
||||
Name string `json:"name" binding:"required,min=1,max=255"`
|
||||
Readonly bool `json:"readonly"`
|
||||
Proxy bool `json:"proxy"`
|
||||
DisableSysFiles bool `json:"disable_sys_files"`
|
||||
}
|
||||
CreateDavAccountParamCtx struct{}
|
||||
)
|
||||
@@ -173,6 +174,10 @@ func (service *CreateDavAccountService) validateAndGetBs(user *ent.User) (*bools
|
||||
boolset.Set(types.DavAccountReadOnly, true, &bs)
|
||||
}
|
||||
|
||||
if service.DisableSysFiles {
|
||||
boolset.Set(types.DavAccountDisableSysFiles, true, &bs)
|
||||
}
|
||||
|
||||
if service.Proxy && user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionWebDAVProxy)) {
|
||||
boolset.Set(types.DavAccountProxy, true, &bs)
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func (service *ShareCreateService) Upsert(c *gin.Context, existed int) (string,
|
||||
}
|
||||
|
||||
base := dep.SettingProvider().SiteURL(c)
|
||||
return explorer.BuildShareLink(share, dep.HashIDEncoder(), base), nil
|
||||
return explorer.BuildShareLink(share, dep.HashIDEncoder(), base, true), nil
|
||||
}
|
||||
|
||||
func DeleteShare(c *gin.Context, shareId int) error {
|
||||
|
||||
@@ -2,6 +2,7 @@ package share
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
@@ -25,7 +26,26 @@ type (
|
||||
)
|
||||
|
||||
func (s *ShortLinkRedirectService) RedirectTo(c *gin.Context) string {
|
||||
return routes.MasterShareLongUrl(s.ID, s.Password).String()
|
||||
shareLongUrl := routes.MasterShareLongUrl(s.ID, s.Password)
|
||||
|
||||
shortLinkQuery := c.Request.URL.Query() // Query in ShortLink, adapt to Cloudreve V3
|
||||
shareLongUrlQuery := shareLongUrl.Query()
|
||||
|
||||
userSpecifiedPath := shortLinkQuery.Get("path")
|
||||
if userSpecifiedPath != "" {
|
||||
masterPath := shareLongUrlQuery.Get("path")
|
||||
masterPath += "/" + strings.TrimPrefix(userSpecifiedPath, "/")
|
||||
|
||||
shareLongUrlQuery.Set("path", masterPath)
|
||||
}
|
||||
|
||||
shortLinkQuery.Del("path") // 防止用户指定的 Path 就是空字符串
|
||||
for k, vals := range shortLinkQuery {
|
||||
shareLongUrlQuery[k] = append(shareLongUrlQuery[k], vals...)
|
||||
}
|
||||
|
||||
shareLongUrl.RawQuery = shareLongUrlQuery.Encode()
|
||||
return shareLongUrl.String()
|
||||
}
|
||||
|
||||
type (
|
||||
@@ -137,6 +157,16 @@ func (s *ListShareService) ListInUserProfile(c *gin.Context, uid int) (*ListShar
|
||||
hasher := dep.HashIDEncoder()
|
||||
shareClient := dep.ShareClient()
|
||||
|
||||
targetUser, err := dep.UserClient().GetActiveByID(c, uid)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get user", err)
|
||||
}
|
||||
|
||||
if targetUser.Settings != nil && targetUser.Settings.ShareLinksInProfile == types.ProfileHideShare {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "User has disabled share links in profile", nil)
|
||||
}
|
||||
|
||||
publicOnly := targetUser.Settings == nil || targetUser.Settings.ShareLinksInProfile == types.ProfilePublicShareOnly
|
||||
args := &inventory.ListShareArgs{
|
||||
PaginationArgs: &inventory.PaginationArgs{
|
||||
UseCursorPagination: true,
|
||||
@@ -146,7 +176,7 @@ func (s *ListShareService) ListInUserProfile(c *gin.Context, uid int) (*ListShar
|
||||
OrderBy: s.OrderBy,
|
||||
},
|
||||
UserID: uid,
|
||||
PublicOnly: true,
|
||||
PublicOnly: publicOnly,
|
||||
}
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadShareUser{}, true)
|
||||
|
||||
@@ -29,6 +29,7 @@ type UserSettings struct {
|
||||
TwoFAEnabled bool `json:"two_fa_enabled"`
|
||||
Passkeys []Passkey `json:"passkeys,omitempty"`
|
||||
DisableViewSync bool `json:"disable_view_sync"`
|
||||
ShareLinksInProfile string `json:"share_links_in_profile"`
|
||||
}
|
||||
|
||||
func BuildUserSettings(u *ent.User, passkeys []*ent.Passkey, parser *uaparser.Parser) *UserSettings {
|
||||
@@ -41,7 +42,8 @@ func BuildUserSettings(u *ent.User, passkeys []*ent.Passkey, parser *uaparser.Pa
|
||||
Passkeys: lo.Map(passkeys, func(item *ent.Passkey, index int) Passkey {
|
||||
return BuildPasskey(item)
|
||||
}),
|
||||
DisableViewSync: u.Settings.DisableViewSync,
|
||||
DisableViewSync: u.Settings.DisableViewSync,
|
||||
ShareLinksInProfile: string(u.Settings.ShareLinksInProfile),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,18 +99,19 @@ type BuiltinLoginResponse struct {
|
||||
|
||||
// User 用户序列化器
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Nickname string `json:"nickname"`
|
||||
Status user.Status `json:"status,omitempty"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
PreferredTheme string `json:"preferred_theme,omitempty"`
|
||||
Anonymous bool `json:"anonymous,omitempty"`
|
||||
Group *Group `json:"group,omitempty"`
|
||||
Pined []types.PinedFile `json:"pined,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
DisableViewSync bool `json:"disable_view_sync,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Nickname string `json:"nickname"`
|
||||
Status user.Status `json:"status,omitempty"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
PreferredTheme string `json:"preferred_theme,omitempty"`
|
||||
Anonymous bool `json:"anonymous,omitempty"`
|
||||
Group *Group `json:"group,omitempty"`
|
||||
Pined []types.PinedFile `json:"pined,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
DisableViewSync bool `json:"disable_view_sync,omitempty"`
|
||||
ShareLinksInProfile types.ShareLinksInProfileLevel `json:"share_links_in_profile,omitempty"`
|
||||
}
|
||||
|
||||
type Group struct {
|
||||
@@ -153,18 +156,19 @@ func BuildWebAuthnList(credentials []webauthn.Credential) []WebAuthnCredentials
|
||||
// BuildUser 序列化用户
|
||||
func BuildUser(user *ent.User, idEncoder hashid.Encoder) User {
|
||||
return User{
|
||||
ID: hashid.EncodeUserID(idEncoder, user.ID),
|
||||
Email: user.Email,
|
||||
Nickname: user.Nick,
|
||||
Status: user.Status,
|
||||
Avatar: user.Avatar,
|
||||
CreatedAt: user.CreatedAt,
|
||||
PreferredTheme: user.Settings.PreferredTheme,
|
||||
Anonymous: user.ID == 0,
|
||||
Group: BuildGroup(user.Edges.Group, idEncoder),
|
||||
Pined: user.Settings.Pined,
|
||||
Language: user.Settings.Language,
|
||||
DisableViewSync: user.Settings.DisableViewSync,
|
||||
ID: hashid.EncodeUserID(idEncoder, user.ID),
|
||||
Email: user.Email,
|
||||
Nickname: user.Nick,
|
||||
Status: user.Status,
|
||||
Avatar: user.Avatar,
|
||||
CreatedAt: user.CreatedAt,
|
||||
PreferredTheme: user.Settings.PreferredTheme,
|
||||
Anonymous: user.ID == 0,
|
||||
Group: BuildGroup(user.Edges.Group, idEncoder),
|
||||
Pined: user.Settings.Pined,
|
||||
Language: user.Settings.Language,
|
||||
DisableViewSync: user.Settings.DisableViewSync,
|
||||
ShareLinksInProfile: user.Settings.ShareLinksInProfile,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,10 +197,11 @@ func BuildUserRedacted(u *ent.User, level int, idEncoder hashid.Encoder) User {
|
||||
userRaw := BuildUser(u, idEncoder)
|
||||
|
||||
user := User{
|
||||
ID: userRaw.ID,
|
||||
Nickname: userRaw.Nickname,
|
||||
Avatar: userRaw.Avatar,
|
||||
CreatedAt: userRaw.CreatedAt,
|
||||
ID: userRaw.ID,
|
||||
Nickname: userRaw.Nickname,
|
||||
Avatar: userRaw.Avatar,
|
||||
CreatedAt: userRaw.CreatedAt,
|
||||
ShareLinksInProfile: userRaw.ShareLinksInProfile,
|
||||
}
|
||||
|
||||
if userRaw.Group != nil {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
@@ -221,6 +222,7 @@ type (
|
||||
TwoFAEnabled *bool `json:"two_fa_enabled" binding:"omitempty"`
|
||||
TwoFACode *string `json:"two_fa_code" binding:"omitempty"`
|
||||
DisableViewSync *bool `json:"disable_view_sync" binding:"omitempty"`
|
||||
ShareLinksInProfile *string `json:"share_links_in_profile" binding:"omitempty"`
|
||||
}
|
||||
PatchUserSettingParamsCtx struct{}
|
||||
)
|
||||
@@ -267,6 +269,11 @@ func (s *PatchUserSetting) Patch(c *gin.Context) error {
|
||||
saveSetting = true
|
||||
}
|
||||
|
||||
if s.ShareLinksInProfile != nil {
|
||||
u.Settings.ShareLinksInProfile = types.ShareLinksInProfileLevel(*s.ShareLinksInProfile)
|
||||
saveSetting = true
|
||||
}
|
||||
|
||||
if s.CurrentPassword != nil && s.NewPassword != nil {
|
||||
if err := inventory.CheckPassword(u, *s.CurrentPassword); err != nil {
|
||||
return serializer.NewError(serializer.CodeIncorrectPassword, "Incorrect password", err)
|
||||
|
||||
Reference in New Issue
Block a user