mirror of
https://github.com/cloudreve/cloudreve.git
synced 2026-03-11 03:37:01 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cdccf5fc9 | ||
|
|
15762cb393 | ||
|
|
e96b595622 | ||
|
|
d19fc0e75c | ||
|
|
195d68c535 | ||
|
|
000124f6c7 | ||
|
|
ca57ca1ba0 | ||
|
|
3cda4d1ef7 | ||
|
|
b13490357b | ||
|
|
617d3a4262 | ||
|
|
75a03aa708 | ||
|
|
fe2ccb4d4e | ||
|
|
aada3aab02 | ||
|
|
a0aefef691 | ||
|
|
17fc598fb3 | ||
|
|
19a65b065c | ||
|
|
e0b2b4649e | ||
|
|
642c32c6cc | ||
|
|
6106b57bc7 | ||
|
|
f38f32f9f5 |
@@ -3,7 +3,7 @@ FROM alpine:latest
|
||||
WORKDIR /cloudreve
|
||||
|
||||
RUN apk update \
|
||||
&& apk add --no-cache tzdata vips-tools ffmpeg libreoffice aria2 supervisor font-noto font-noto-cjk libheif\
|
||||
&& apk add --no-cache tzdata vips-tools ffmpeg libreoffice aria2 supervisor font-noto font-noto-cjk libheif libraw-tools\
|
||||
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& echo "Asia/Shanghai" > /etc/timezone \
|
||||
&& mkdir -p ./data/temp/aria2 \
|
||||
@@ -13,7 +13,8 @@ ENV CR_ENABLE_ARIA2=1 \
|
||||
CR_SETTING_DEFAULT_thumb_ffmpeg_enabled=1 \
|
||||
CR_SETTING_DEFAULT_thumb_vips_enabled=1 \
|
||||
CR_SETTING_DEFAULT_thumb_libreoffice_enabled=1 \
|
||||
CR_SETTING_DEFAULT_media_meta_ffprobe=1
|
||||
CR_SETTING_DEFAULT_media_meta_ffprobe=1 \
|
||||
CR_SETTING_DEFAULT_thumb_libraw_enabled=1
|
||||
|
||||
COPY .build/aria2.supervisor.conf .build/entrypoint.sh ./
|
||||
COPY cloudreve ./cloudreve
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
## :sparkles: Features
|
||||
|
||||
- :cloud: Support storing files into Local, Remote node, OneDrive, S3 compatible API, Qiniu, Aliyun OSS, Tencent COS, Upyun, OneDrive.
|
||||
- :cloud: Support storing files into Local, Remote node, OneDrive, S3 compatible API, Qiniu, Aliyun OSS, Tencent COS, Upyun.
|
||||
- :outbox_tray: Upload/Download in directly transmission from client to storage providers.
|
||||
- 💾 Integrate with Aria2/qBittorrent to download files in background, use multiple download nodes to share the load.
|
||||
- 📚 Compress/Extract files, download files in batch.
|
||||
|
||||
2
assets
2
assets
Submodule assets updated: ac6f97d9ba...0b49582a07
@@ -1,5 +1,5 @@
|
||||
services:
|
||||
pro:
|
||||
cloudreve:
|
||||
image: cloudreve/cloudreve:latest
|
||||
container_name: cloudreve-backend
|
||||
depends_on:
|
||||
|
||||
4
go.mod
4
go.mod
@@ -16,7 +16,7 @@ require (
|
||||
github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d
|
||||
github.com/dsoprea/go-tiff-image-structure v0.0.0-20221003165014-8ecc4f52edca
|
||||
github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf
|
||||
github.com/fatih/color v1.9.0
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/gin-contrib/cors v1.3.0
|
||||
github.com/gin-contrib/sessions v1.0.2
|
||||
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2
|
||||
@@ -110,7 +110,7 @@ require (
|
||||
github.com/klauspost/pgzip v1.2.5 // 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.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
|
||||
9
go.sum
9
go.sum
@@ -274,8 +274,9 @@ github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DP
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||
github.com/etcd-io/gofail v0.0.0-20190801230047-ad7f989257ca/go.mod h1:49H/RkXP8pKaZy4h0d+NW16rSLhyVBt4o6VLJbmOqDE=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||
github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||
@@ -658,8 +659,8 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
@@ -669,6 +670,7 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
@@ -1248,6 +1250,7 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
||||
@@ -52,6 +52,9 @@ func NewRawEntClient(l logging.Logger, config conf.ConfigProvider) (*ent.Client,
|
||||
if confDBType == conf.SQLite3DB || confDBType == "" {
|
||||
confDBType = conf.SQLiteDB
|
||||
}
|
||||
if confDBType == conf.MariaDB {
|
||||
confDBType = conf.MySqlDB
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
@@ -75,7 +78,7 @@ func NewRawEntClient(l logging.Logger, config conf.ConfigProvider) (*ent.Client,
|
||||
client, err = sql.Open("sqlite3", util.RelativePath(dbConfig.DBFile))
|
||||
case conf.PostgresDB:
|
||||
l.Info("Connect to Postgres database %q.", dbConfig.Host)
|
||||
client, err = sql.Open("postgres", fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=allow",
|
||||
client, err = sql.Open("postgres", fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable",
|
||||
dbConfig.Host,
|
||||
dbConfig.User,
|
||||
dbConfig.Password,
|
||||
|
||||
@@ -59,11 +59,17 @@ type (
|
||||
StoragePolicyID int
|
||||
}
|
||||
|
||||
MetadataFilter struct {
|
||||
Key string
|
||||
Value string
|
||||
Exact bool
|
||||
}
|
||||
|
||||
SearchFileParameters struct {
|
||||
Name []string
|
||||
// NameOperatorOr is true if the name should match any of the given names, false if all of them
|
||||
NameOperatorOr bool
|
||||
Metadata map[string]string
|
||||
Metadata []MetadataFilter
|
||||
Type *types.FileType
|
||||
UseFullText bool
|
||||
CaseFolding bool
|
||||
|
||||
@@ -16,6 +16,10 @@ import (
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
const (
|
||||
metadataExactMatchPrefix = "!exact:"
|
||||
)
|
||||
|
||||
func (f *fileClient) searchQuery(q *ent.FileQuery, args *SearchFileParameters, parents []*ent.File, ownerId int) *ent.FileQuery {
|
||||
if len(parents) == 1 && parents[0] == nil {
|
||||
q = q.Where(file.OwnerID(ownerId))
|
||||
@@ -69,13 +73,17 @@ func (f *fileClient) searchQuery(q *ent.FileQuery, args *SearchFileParameters, p
|
||||
}
|
||||
|
||||
if len(args.Metadata) > 0 {
|
||||
metaPredicates := lo.MapToSlice(args.Metadata, func(name string, value string) predicate.Metadata {
|
||||
nameEq := metadata.NameEQ(value)
|
||||
if name == "" {
|
||||
metaPredicates := lo.Map(args.Metadata, func(item MetadataFilter, index int) predicate.Metadata {
|
||||
if item.Exact {
|
||||
return metadata.And(metadata.NameEQ(item.Key), metadata.ValueEQ(item.Value))
|
||||
}
|
||||
|
||||
nameEq := metadata.NameEQ(item.Key)
|
||||
if item.Value == "" {
|
||||
return nameEq
|
||||
} else {
|
||||
valueContain := metadata.ValueContainsFold(value)
|
||||
return metadata.And(metadata.NameEQ(name), valueContain)
|
||||
valueContain := metadata.ValueContainsFold(item.Value)
|
||||
return metadata.And(nameEq, valueContain)
|
||||
}
|
||||
})
|
||||
metaPredicates = append(metaPredicates, metadata.IsPublic(true))
|
||||
|
||||
@@ -85,8 +85,8 @@ func (c *groupClient) Upsert(ctx context.Context, group *ent.Group) (*ent.Group,
|
||||
SetPermissions(group.Permissions).
|
||||
SetSettings(group.Settings)
|
||||
|
||||
if group.StoragePolicyID > 0 {
|
||||
stm.SetStoragePolicyID(group.StoragePolicyID)
|
||||
if group.Edges.StoragePolicies != nil && group.Edges.StoragePolicies.ID > 0 {
|
||||
stm.SetStoragePolicyID(group.Edges.StoragePolicies.ID)
|
||||
}
|
||||
|
||||
return stm.Save(ctx)
|
||||
|
||||
@@ -27,6 +27,7 @@ type (
|
||||
SkipStoragePolicyCache struct{}
|
||||
|
||||
StoragePolicyClient interface {
|
||||
TxOperator
|
||||
// GetByGroup returns the storage policies of the group.
|
||||
GetByGroup(ctx context.Context, group *ent.Group) (*ent.StoragePolicy, error)
|
||||
// GetPolicyByID returns the storage policy by id.
|
||||
@@ -64,6 +65,14 @@ type storagePolicyClient struct {
|
||||
cache cache.Driver
|
||||
}
|
||||
|
||||
func (c *storagePolicyClient) SetClient(newClient *ent.Client) TxOperator {
|
||||
return &storagePolicyClient{client: newClient, cache: c.cache}
|
||||
}
|
||||
|
||||
func (c *storagePolicyClient) GetClient() *ent.Client {
|
||||
return c.client
|
||||
}
|
||||
|
||||
func (c *storagePolicyClient) Delete(ctx context.Context, policy *ent.StoragePolicy) error {
|
||||
if err := c.client.StoragePolicy.DeleteOne(policy).Exec(ctx); err != nil {
|
||||
return fmt.Errorf("failed to delete storage policy: %w", err)
|
||||
|
||||
@@ -324,6 +324,22 @@ var (
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
defaultFileProps = []types.CustomProps{
|
||||
{
|
||||
ID: "description",
|
||||
Type: types.CustomPropsTypeText,
|
||||
Name: "fileManager.description",
|
||||
Icon: "fluent:slide-text-24-filled",
|
||||
},
|
||||
{
|
||||
ID: "rating",
|
||||
Type: types.CustomPropsTypeRating,
|
||||
Name: "fileManager.rating",
|
||||
Icon: "fluent:data-bar-vertical-star-24-filled",
|
||||
Max: 5,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
var DefaultSettings = map[string]string{
|
||||
@@ -395,6 +411,7 @@ var DefaultSettings = map[string]string{
|
||||
"captcha_cap_instance_url": "",
|
||||
"captcha_cap_site_key": "",
|
||||
"captcha_cap_secret_key": "",
|
||||
"captcha_cap_asset_server": "jsdelivr",
|
||||
"thumb_width": "400",
|
||||
"thumb_height": "300",
|
||||
"thumb_entity_suffix": "._thumb",
|
||||
@@ -413,6 +430,7 @@ var DefaultSettings = map[string]string{
|
||||
"thumb_ffmpeg_max_size": "10737418240", // 10 GB
|
||||
"thumb_ffmpeg_exts": "3g2,3gp,asf,asx,avi,divx,flv,m2ts,m2v,m4v,mkv,mov,mp4,mpeg,mpg,mts,mxf,ogv,rm,swf,webm,wmv",
|
||||
"thumb_ffmpeg_seek": "00:00:01.00",
|
||||
"thumb_ffmpeg_extra_args": "-hwaccel auto",
|
||||
"thumb_libreoffice_path": "soffice",
|
||||
"thumb_libreoffice_max_size": "78643200", // 75 MB
|
||||
"thumb_libreoffice_enabled": "0",
|
||||
@@ -500,6 +518,10 @@ var DefaultSettings = map[string]string{
|
||||
"qq_login": `0`,
|
||||
"qq_login_config": `{"direct_sign_in":false}`,
|
||||
"license": "",
|
||||
"custom_nav_items": "[]",
|
||||
"headless_footer_html": "",
|
||||
"headless_bottom_html": "",
|
||||
"sidebar_bottom_html": "",
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -515,4 +537,10 @@ func init() {
|
||||
}
|
||||
|
||||
DefaultSettings["file_viewers"] = string(viewers)
|
||||
|
||||
customProps, err := json.Marshal(defaultFileProps)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
DefaultSettings["custom_props"] = string(customProps)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package inventory
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
@@ -44,6 +45,8 @@ type TaskClient interface {
|
||||
List(ctx context.Context, args *ListTaskArgs) (*ListTaskResult, error)
|
||||
// DeleteByIDs deletes the tasks with the given IDs.
|
||||
DeleteByIDs(ctx context.Context, ids ...int) error
|
||||
// DeleteBy deletes the tasks with the given args.
|
||||
DeleteBy(ctx context.Context, args *DeleteTaskArgs) error
|
||||
}
|
||||
|
||||
type (
|
||||
@@ -59,6 +62,12 @@ type (
|
||||
*PaginationResults
|
||||
Tasks []*ent.Task
|
||||
}
|
||||
|
||||
DeleteTaskArgs struct {
|
||||
NotAfter time.Time
|
||||
Types []string
|
||||
Status []task.Status
|
||||
}
|
||||
)
|
||||
|
||||
func NewTaskClient(client *ent.Client, dbType conf.DBType, hasher hashid.Encoder) TaskClient {
|
||||
@@ -113,6 +122,23 @@ func (c *taskClient) DeleteByIDs(ctx context.Context, ids ...int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *taskClient) DeleteBy(ctx context.Context, args *DeleteTaskArgs) error {
|
||||
query := c.client.Task.
|
||||
Delete().
|
||||
Where(task.CreatedAtLTE(args.NotAfter))
|
||||
|
||||
if len(args.Status) > 0 {
|
||||
query.Where(task.StatusIn(args.Status...))
|
||||
}
|
||||
|
||||
if len(args.Types) > 0 {
|
||||
query.Where(task.TypeIn(args.Types...))
|
||||
}
|
||||
|
||||
_, err := query.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *taskClient) Update(ctx context.Context, task *ent.Task, args *TaskArgs) (*ent.Task, error) {
|
||||
stm := c.client.Task.UpdateOne(task).
|
||||
SetPublicState(args.PublicState)
|
||||
|
||||
@@ -90,6 +90,8 @@ type (
|
||||
UseCname bool `json:"use_cname,omitempty"`
|
||||
// CDN domain does not need to be signed.
|
||||
SourceAuth bool `json:"source_auth,omitempty"`
|
||||
// QiniuUploadCdn whether to use CDN for Qiniu upload.
|
||||
QiniuUploadCdn bool `json:"qiniu_upload_cdn,omitempty"`
|
||||
}
|
||||
|
||||
FileType int
|
||||
@@ -171,12 +173,15 @@ type (
|
||||
}
|
||||
|
||||
ColumTypeProps struct {
|
||||
MetadataKey string `json:"metadata_key,omitempty" binding:"max=255"`
|
||||
MetadataKey string `json:"metadata_key,omitempty" binding:"max=255"`
|
||||
CustomPropsID string `json:"custom_props_id,omitempty" binding:"max=255"`
|
||||
}
|
||||
|
||||
ShareProps struct {
|
||||
// Whether to share view setting from owner
|
||||
ShareView bool `json:"share_view,omitempty"`
|
||||
// Whether to automatically show readme file in share view
|
||||
ShowReadMe bool `json:"show_read_me,omitempty"`
|
||||
}
|
||||
|
||||
FileTypeIconSetting struct {
|
||||
@@ -274,25 +279,51 @@ const (
|
||||
ViewerTypeCustom = "custom"
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
||||
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"`
|
||||
}
|
||||
ViewerGroup struct {
|
||||
Viewers []Viewer `json:"viewers"`
|
||||
}
|
||||
|
||||
type ViewerGroup struct {
|
||||
Viewers []Viewer `json:"viewers"`
|
||||
}
|
||||
NewFileTemplate struct {
|
||||
Ext string `json:"ext"`
|
||||
DisplayName string `json:"display_name"`
|
||||
}
|
||||
)
|
||||
|
||||
type NewFileTemplate struct {
|
||||
Ext string `json:"ext"`
|
||||
DisplayName string `json:"display_name"`
|
||||
}
|
||||
type (
|
||||
CustomPropsType string
|
||||
CustomProps struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type CustomPropsType `json:"type"`
|
||||
Max int `json:"max,omitempty"`
|
||||
Min int `json:"min,omitempty"`
|
||||
Default string `json:"default,omitempty"`
|
||||
Options []string `json:"options,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
CustomPropsTypeText = "text"
|
||||
CustomPropsTypeNumber = "number"
|
||||
CustomPropsTypeBoolean = "boolean"
|
||||
CustomPropsTypeSelect = "select"
|
||||
CustomPropsTypeMultiSelect = "multi_select"
|
||||
CustomPropsTypeLink = "link"
|
||||
CustomPropsTypeRating = "rating"
|
||||
)
|
||||
|
||||
@@ -220,5 +220,8 @@ func getUrlSignContent(ctx context.Context, url *url.URL) string {
|
||||
// host = strings.TrimSuffix(host, "/")
|
||||
// // remove port if it exists
|
||||
// host = strings.Split(host, ":")[0]
|
||||
if url.Path == "" {
|
||||
return "/"
|
||||
}
|
||||
return url.Path
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ var (
|
||||
MySqlDB DBType = "mysql"
|
||||
MsSqlDB DBType = "mssql"
|
||||
PostgresDB DBType = "postgres"
|
||||
MariaDB DBType = "mariadb"
|
||||
)
|
||||
|
||||
// Database 数据库
|
||||
|
||||
@@ -67,7 +67,10 @@ func New(ctx context.Context, policy *ent.StoragePolicy, settings setting.Provid
|
||||
}
|
||||
|
||||
mac := qbox.NewMac(policy.AccessKey, policy.SecretKey)
|
||||
cfg := &storage.Config{UseHTTPS: true}
|
||||
cfg := &storage.Config{
|
||||
UseHTTPS: true,
|
||||
UseCdnDomains: policy.Settings.QiniuUploadCdn,
|
||||
}
|
||||
|
||||
driver := &Driver{
|
||||
policy: policy,
|
||||
|
||||
@@ -25,6 +25,7 @@ const (
|
||||
QuerySearchNameOpOr = "name_op_or"
|
||||
QuerySearchUseOr = "use_or"
|
||||
QuerySearchMetadataPrefix = "meta_"
|
||||
QuerySearchMetadataExact = "exact_meta_"
|
||||
QuerySearchCaseFolding = "case_folding"
|
||||
QuerySearchType = "type"
|
||||
QuerySearchTypeCategory = "category"
|
||||
@@ -218,7 +219,7 @@ func (u *URI) FileSystem() constants.FileSystemType {
|
||||
func (u *URI) SearchParameters() *inventory.SearchFileParameters {
|
||||
q := u.U.Query()
|
||||
res := &inventory.SearchFileParameters{
|
||||
Metadata: make(map[string]string),
|
||||
Metadata: make([]inventory.MetadataFilter, 0),
|
||||
}
|
||||
withSearch := false
|
||||
|
||||
@@ -252,7 +253,18 @@ func (u *URI) SearchParameters() *inventory.SearchFileParameters {
|
||||
|
||||
for k, v := range q {
|
||||
if strings.HasPrefix(k, QuerySearchMetadataPrefix) {
|
||||
res.Metadata[strings.TrimPrefix(k, QuerySearchMetadataPrefix)] = v[0]
|
||||
res.Metadata = append(res.Metadata, inventory.MetadataFilter{
|
||||
Key: strings.TrimPrefix(k, QuerySearchMetadataPrefix),
|
||||
Value: v[0],
|
||||
Exact: false,
|
||||
})
|
||||
withSearch = true
|
||||
} else if strings.HasPrefix(k, QuerySearchMetadataExact) {
|
||||
res.Metadata = append(res.Metadata, inventory.MetadataFilter{
|
||||
Key: strings.TrimPrefix(k, QuerySearchMetadataExact),
|
||||
Value: v[0],
|
||||
Exact: true,
|
||||
})
|
||||
withSearch = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ func (m *manager) GetUrlForRedirectedDirectLink(ctx context.Context, dl *ent.Dir
|
||||
)
|
||||
|
||||
// Try to read from cache.
|
||||
cacheKey := entityUrlCacheKey(primaryEntity.ID(), int64(dl.Speed), dl.Name, false,
|
||||
cacheKey := entityUrlCacheKey(primaryEntity.ID(), int64(dl.Speed), dl.Name, o.IsDownload,
|
||||
m.settings.SiteURL(ctx).String())
|
||||
if cached, ok := m.kv.Get(cacheKey); ok {
|
||||
cachedItem := cached.(EntityUrlCache)
|
||||
@@ -185,7 +185,7 @@ func (m *manager) GetUrlForRedirectedDirectLink(ctx context.Context, dl *ent.Dir
|
||||
m.l, m.config, m.dep.MimeDetector(ctx))
|
||||
downloadUrl, err := source.Url(ctx,
|
||||
entitysource.WithExpire(o.Expire),
|
||||
entitysource.WithDownload(false),
|
||||
entitysource.WithDownload(o.IsDownload),
|
||||
entitysource.WithSpeedLimit(int64(dl.Speed)),
|
||||
entitysource.WithDisplayName(dl.Name),
|
||||
)
|
||||
|
||||
@@ -115,6 +115,7 @@ type (
|
||||
RemainDownloads int
|
||||
Expire *time.Time
|
||||
ShareView bool
|
||||
ShowReadMe bool
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -5,14 +5,18 @@ import (
|
||||
"crypto/sha1"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/application/constants"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"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/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"strings"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -20,13 +24,14 @@ type (
|
||||
)
|
||||
|
||||
const (
|
||||
wildcardMetadataKey = "*"
|
||||
customizeMetadataSuffix = "customize"
|
||||
tagMetadataSuffix = "tag"
|
||||
iconColorMetadataKey = customizeMetadataSuffix + ":icon_color"
|
||||
emojiIconMetadataKey = customizeMetadataSuffix + ":emoji"
|
||||
shareOwnerMetadataKey = dbfs.MetadataSysPrefix + "shared_owner"
|
||||
shareRedirectMetadataKey = dbfs.MetadataSysPrefix + "shared_redirect"
|
||||
wildcardMetadataKey = "*"
|
||||
customizeMetadataSuffix = "customize"
|
||||
tagMetadataSuffix = "tag"
|
||||
customPropsMetadataSuffix = "props"
|
||||
iconColorMetadataKey = customizeMetadataSuffix + ":icon_color"
|
||||
emojiIconMetadataKey = customizeMetadataSuffix + ":emoji"
|
||||
shareOwnerMetadataKey = dbfs.MetadataSysPrefix + "shared_owner"
|
||||
shareRedirectMetadataKey = dbfs.MetadataSysPrefix + "shared_redirect"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -131,6 +136,126 @@ var (
|
||||
return nil
|
||||
},
|
||||
},
|
||||
customPropsMetadataSuffix: {
|
||||
wildcardMetadataKey: func(ctx context.Context, m *manager, patch *fs.MetadataPatch) error {
|
||||
if patch.Remove {
|
||||
return nil
|
||||
}
|
||||
|
||||
customProps := m.settings.CustomProps(ctx)
|
||||
propId := strings.TrimPrefix(patch.Key, customPropsMetadataSuffix+":")
|
||||
for _, prop := range customProps {
|
||||
if prop.ID == propId {
|
||||
switch prop.Type {
|
||||
case types.CustomPropsTypeText:
|
||||
if prop.Min > 0 && prop.Min > len(patch.Value) {
|
||||
return fmt.Errorf("value is too short")
|
||||
}
|
||||
if prop.Max > 0 && prop.Max < len(patch.Value) {
|
||||
return fmt.Errorf("value is too long")
|
||||
}
|
||||
|
||||
return nil
|
||||
case types.CustomPropsTypeRating:
|
||||
if patch.Value == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validate the value is a number
|
||||
rating, err := strconv.Atoi(patch.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("value is not a number")
|
||||
}
|
||||
|
||||
if prop.Max < rating {
|
||||
return fmt.Errorf("value is too large")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case types.CustomPropsTypeNumber:
|
||||
if patch.Value == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
value, err := strconv.Atoi(patch.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("value is not a number")
|
||||
}
|
||||
|
||||
if prop.Min > value {
|
||||
return fmt.Errorf("value is too small")
|
||||
}
|
||||
if prop.Max > 0 && prop.Max < value {
|
||||
return fmt.Errorf("value is too large")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case types.CustomPropsTypeBoolean:
|
||||
if patch.Value == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if patch.Value != "true" && patch.Value != "false" {
|
||||
return fmt.Errorf("value is not a boolean")
|
||||
}
|
||||
|
||||
return nil
|
||||
case types.CustomPropsTypeSelect:
|
||||
if patch.Value == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, option := range prop.Options {
|
||||
if option == patch.Value {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("invalid option")
|
||||
case types.CustomPropsTypeMultiSelect:
|
||||
if patch.Value == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var values []string
|
||||
if err := json.Unmarshal([]byte(patch.Value), &values); err != nil {
|
||||
return fmt.Errorf("invalid multi select value: %w", err)
|
||||
}
|
||||
|
||||
// make sure all values are in the options
|
||||
for _, value := range values {
|
||||
if !lo.Contains(prop.Options, value) {
|
||||
return fmt.Errorf("invalid option")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case types.CustomPropsTypeLink:
|
||||
if patch.Value == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if prop.Min > 0 && len(patch.Value) < prop.Min {
|
||||
return fmt.Errorf("value is too small")
|
||||
}
|
||||
|
||||
if prop.Max > 0 && len(patch.Value) > prop.Max {
|
||||
return fmt.Errorf("value is too large")
|
||||
}
|
||||
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("unkown custom props")
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -267,7 +267,8 @@ func (l *manager) CreateOrUpdateShare(ctx context.Context, path *fs.URI, args *C
|
||||
}
|
||||
|
||||
props := &types.ShareProps{
|
||||
ShareView: args.ShareView,
|
||||
ShareView: args.ShareView,
|
||||
ShowReadMe: args.ShowReadMe,
|
||||
}
|
||||
|
||||
share, err := shareClient.Upsert(ctx, &inventory.CreateShareParams{
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -318,7 +319,7 @@ func (m *RemoteDownloadTask) slaveTransfer(ctx context.Context, dep dependency.D
|
||||
continue
|
||||
}
|
||||
|
||||
dst := dstUri.JoinRaw(f.Name)
|
||||
dst := dstUri.JoinRaw(sanitizeFileName(f.Name))
|
||||
src := path.Join(m.state.Status.SavePath, f.Name)
|
||||
payload.Files = append(payload.Files, SlaveUploadEntity{
|
||||
Src: src,
|
||||
@@ -437,9 +438,10 @@ func (m *RemoteDownloadTask) masterTransfer(ctx context.Context, dep dependency.
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
dst := dstUri.JoinRaw(file.Name)
|
||||
sanitizedName := sanitizeFileName(file.Name)
|
||||
dst := dstUri.JoinRaw(sanitizedName)
|
||||
src := filepath.FromSlash(path.Join(m.state.Status.SavePath, file.Name))
|
||||
m.l.Info("Uploading file %s to %s...", src, file.Name, dst)
|
||||
m.l.Info("Uploading file %s to %s...", src, sanitizedName, dst)
|
||||
|
||||
progressKey := fmt.Sprintf("%s%d", ProgressTypeUploadSinglePrefix, workerId)
|
||||
m.Lock()
|
||||
@@ -538,7 +540,7 @@ func (m *RemoteDownloadTask) validateFiles(ctx context.Context, dep dependency.D
|
||||
|
||||
validateArgs := lo.Map(selectedFiles, func(f downloader.TaskFile, _ int) fs.PreValidateFile {
|
||||
return fs.PreValidateFile{
|
||||
Name: f.Name,
|
||||
Name: sanitizeFileName(f.Name),
|
||||
Size: f.Size,
|
||||
OmitName: f.Name == "",
|
||||
}
|
||||
@@ -637,3 +639,8 @@ func (m *RemoteDownloadTask) Progress(ctx context.Context) queue.Progresses {
|
||||
}
|
||||
return m.progress
|
||||
}
|
||||
|
||||
func sanitizeFileName(name string) string {
|
||||
r := strings.NewReplacer("\\", "_", ":", "_", "*", "_", "?", "_", "\"", "_", "<", "_", ">", "_", "|", "_")
|
||||
return r.Replace(name)
|
||||
}
|
||||
|
||||
@@ -196,6 +196,14 @@ type (
|
||||
LibRawThumbExts(ctx context.Context) []string
|
||||
// LibRawThumbPath returns the path of libraw executable.
|
||||
LibRawThumbPath(ctx context.Context) string
|
||||
// CustomProps returns the custom props settings.
|
||||
CustomProps(ctx context.Context) []types.CustomProps
|
||||
// CustomNavItems returns the custom nav items settings.
|
||||
CustomNavItems(ctx context.Context) []CustomNavItem
|
||||
// CustomHTML returns the custom HTML settings.
|
||||
CustomHTML(ctx context.Context) *CustomHTML
|
||||
// FFMpegExtraArgs returns the extra arguments of ffmpeg thumb generator.
|
||||
FFMpegExtraArgs(ctx context.Context) string
|
||||
}
|
||||
UseFirstSiteUrlCtxKey = struct{}
|
||||
)
|
||||
@@ -223,6 +231,30 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
func (s *settingProvider) CustomHTML(ctx context.Context) *CustomHTML {
|
||||
return &CustomHTML{
|
||||
HeadlessFooter: s.getString(ctx, "headless_footer_html", ""),
|
||||
HeadlessBody: s.getString(ctx, "headless_bottom_html", ""),
|
||||
SidebarBottom: s.getString(ctx, "sidebar_bottom_html", ""),
|
||||
}
|
||||
}
|
||||
func (s *settingProvider) CustomNavItems(ctx context.Context) []CustomNavItem {
|
||||
raw := s.getString(ctx, "custom_nav_items", "[]")
|
||||
var items []CustomNavItem
|
||||
if err := json.Unmarshal([]byte(raw), &items); err != nil {
|
||||
return []CustomNavItem{}
|
||||
}
|
||||
return items
|
||||
}
|
||||
func (s *settingProvider) CustomProps(ctx context.Context) []types.CustomProps {
|
||||
raw := s.getString(ctx, "custom_props", "[]")
|
||||
var props []types.CustomProps
|
||||
if err := json.Unmarshal([]byte(raw), &props); err != nil {
|
||||
return []types.CustomProps{}
|
||||
}
|
||||
return props
|
||||
}
|
||||
|
||||
func (s *settingProvider) License(ctx context.Context) string {
|
||||
return s.getString(ctx, "license", "")
|
||||
}
|
||||
@@ -376,6 +408,10 @@ func (s *settingProvider) FFMpegThumbSeek(ctx context.Context) string {
|
||||
return s.getString(ctx, "thumb_ffmpeg_seek", "00:00:01.00")
|
||||
}
|
||||
|
||||
func (s *settingProvider) FFMpegExtraArgs(ctx context.Context) string {
|
||||
return s.getString(ctx, "thumb_ffmpeg_extra_args", "")
|
||||
}
|
||||
|
||||
func (s *settingProvider) FFMpegThumbMaxSize(ctx context.Context) int64 {
|
||||
return s.getInt64(ctx, "thumb_ffmpeg_max_size", 10737418240)
|
||||
}
|
||||
@@ -671,6 +707,7 @@ func (s *settingProvider) CapCaptcha(ctx context.Context) *Cap {
|
||||
InstanceURL: s.getString(ctx, "captcha_cap_instance_url", ""),
|
||||
SiteKey: s.getString(ctx, "captcha_cap_site_key", ""),
|
||||
SecretKey: s.getString(ctx, "captcha_cap_secret_key", ""),
|
||||
AssetServer: s.getString(ctx, "captcha_cap_asset_server", "jsdelivr"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ type Cap struct {
|
||||
InstanceURL string
|
||||
SiteKey string
|
||||
SecretKey string
|
||||
AssetServer string
|
||||
}
|
||||
|
||||
type SMTP struct {
|
||||
@@ -208,3 +209,15 @@ type AvatarProcess struct {
|
||||
MaxFileSize int64 `json:"max_file_size"`
|
||||
MaxWidth int `json:"max_width"`
|
||||
}
|
||||
|
||||
type CustomNavItem struct {
|
||||
Icon string `json:"icon"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type CustomHTML struct {
|
||||
HeadlessFooter string `json:"headless_footer,omitempty"`
|
||||
HeadlessBody string `json:"headless_bottom,omitempty"`
|
||||
SidebarBottom string `json:"sidebar_bottom,omitempty"`
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver"
|
||||
@@ -41,7 +42,7 @@ func (f *FfmpegGenerator) Generate(ctx context.Context, es entitysource.EntitySo
|
||||
tempOutputPath := filepath.Join(
|
||||
util.DataPath(f.settings.TempPath(ctx)),
|
||||
thumbTempFolder,
|
||||
fmt.Sprintf("thumb_%s.%s", uuid.Must(uuid.NewV4()).String(), f.settings.ThumbEncode(ctx).Format),
|
||||
fmt.Sprintf("thumb_%s.png", uuid.Must(uuid.NewV4()).String()),
|
||||
)
|
||||
|
||||
if err := util.CreatNestedFolder(filepath.Dir(tempOutputPath)); err != nil {
|
||||
@@ -64,9 +65,22 @@ func (f *FfmpegGenerator) Generate(ctx context.Context, es entitysource.EntitySo
|
||||
// Invoke ffmpeg
|
||||
w, h := f.settings.ThumbSize(ctx)
|
||||
scaleOpt := fmt.Sprintf("scale=%d:%d:force_original_aspect_ratio=decrease", w, h)
|
||||
cmd := exec.CommandContext(ctx,
|
||||
f.settings.FFMpegPath(ctx), "-ss", f.settings.FFMpegThumbSeek(ctx), "-i", input,
|
||||
"-vf", scaleOpt, "-vframes", "1", tempOutputPath)
|
||||
args := []string{
|
||||
"-ss", f.settings.FFMpegThumbSeek(ctx),
|
||||
}
|
||||
|
||||
extraArgs := f.settings.FFMpegExtraArgs(ctx)
|
||||
if extraArgs != "" {
|
||||
args = append(args, strings.Split(extraArgs, " ")...)
|
||||
}
|
||||
|
||||
args = append(args, []string{
|
||||
"-i", input,
|
||||
"-vf", scaleOpt,
|
||||
"-vframes", "1",
|
||||
tempOutputPath,
|
||||
}...)
|
||||
cmd := exec.CommandContext(ctx, f.settings.FFMpegPath(ctx), args...)
|
||||
|
||||
// Redirect IO
|
||||
var stdErr bytes.Buffer
|
||||
|
||||
@@ -69,10 +69,9 @@ func (l *LibreOfficeGenerator) Generate(ctx context.Context, es entitysource.Ent
|
||||
}
|
||||
|
||||
// Convert the document to an image
|
||||
encode := l.settings.ThumbEncode(ctx)
|
||||
cmd := exec.CommandContext(ctx, l.settings.LibreOfficePath(ctx), "--headless",
|
||||
"--nologo", "--nofirststartwizard", "--invisible", "--norestore", "--convert-to",
|
||||
encode.Format, "--outdir", tempOutputPath, tempInputPath)
|
||||
"png", "--outdir", tempOutputPath, tempInputPath)
|
||||
|
||||
// Redirect IO
|
||||
var stdErr bytes.Buffer
|
||||
@@ -86,7 +85,7 @@ func (l *LibreOfficeGenerator) Generate(ctx context.Context, es entitysource.Ent
|
||||
return &Result{
|
||||
Path: filepath.Join(
|
||||
tempOutputPath,
|
||||
strings.TrimSuffix(filepath.Base(tempInputPath), filepath.Ext(tempInputPath))+"."+encode.Format,
|
||||
strings.TrimSuffix(filepath.Base(tempInputPath), filepath.Ext(tempInputPath))+".png",
|
||||
),
|
||||
Continue: true,
|
||||
Cleanup: []func(){func() { _ = os.RemoveAll(tempOutputPath) }},
|
||||
|
||||
@@ -38,8 +38,8 @@ func (v *VipsGenerator) Generate(ctx context.Context, es entitysource.EntitySour
|
||||
|
||||
outputOpt := ".png"
|
||||
encode := v.settings.ThumbEncode(ctx)
|
||||
if encode.Format == "jpg" {
|
||||
outputOpt = fmt.Sprintf(".jpg[Q=%d]", encode.Quality)
|
||||
if encode.Format == "jpg" || encode.Format == "webp" {
|
||||
outputOpt = fmt.Sprintf(".%s[Q=%d]", encode.Format, encode.Quality)
|
||||
}
|
||||
|
||||
input := "[descriptor=0]"
|
||||
|
||||
@@ -518,6 +518,17 @@ func AdminBatchDeleteEntity(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func AdminCleanupTask(c *gin.Context) {
|
||||
service := ParametersFromContext[*admin.CleanupTaskService](c, admin.CleanupTaskParameterCtx{})
|
||||
err := service.CleanupTask(c)
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.Err(c, err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, serializer.Response{})
|
||||
}
|
||||
|
||||
func AdminListTasks(c *gin.Context) {
|
||||
service := ParametersFromContext[*admin.AdminListService](c, admin.AdminListServiceParamsCtx{})
|
||||
res, err := service.Tasks(c)
|
||||
|
||||
@@ -86,12 +86,14 @@ func ExtractArchive(c *gin.Context) {
|
||||
}
|
||||
|
||||
// AnonymousPermLink 文件中转后的永久直链接
|
||||
func AnonymousPermLink(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
if err := explorer.RedirectDirectLink(c, name); err != nil {
|
||||
c.JSON(404, serializer.Err(c, err))
|
||||
c.Abort()
|
||||
return
|
||||
func AnonymousPermLink(download bool) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
if err := explorer.RedirectDirectLink(c, name, download); err != nil {
|
||||
c.JSON(404, serializer.Err(c, err))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -245,7 +245,10 @@ func initMasterRouter(dep dependency.Dep) *gin.Engine {
|
||||
{
|
||||
source.GET(":id/:name",
|
||||
middleware.HashID(hashid.SourceLinkID),
|
||||
controllers.AnonymousPermLink)
|
||||
controllers.AnonymousPermLink(false))
|
||||
source.GET("d/:id/:name",
|
||||
middleware.HashID(hashid.SourceLinkID),
|
||||
controllers.AnonymousPermLink(true))
|
||||
}
|
||||
|
||||
shareShort := r.Group("s")
|
||||
@@ -854,6 +857,11 @@ func initMasterRouter(dep dependency.Dep) *gin.Engine {
|
||||
controllers.FromJSON[adminsvc.BatchTaskService](adminsvc.BatchTaskParamCtx{}),
|
||||
controllers.AdminBatchDeleteTask,
|
||||
)
|
||||
// Cleanup tasks
|
||||
queue.POST("cleanup",
|
||||
controllers.FromJSON[adminsvc.CleanupTaskService](adminsvc.CleanupTaskParameterCtx{}),
|
||||
controllers.AdminCleanupTask,
|
||||
)
|
||||
// // 列出任务
|
||||
// queue.POST("list", controllers.AdminListTask)
|
||||
// // 新建文件导入任务
|
||||
|
||||
@@ -294,11 +294,22 @@ func (service *UpdateStoragePolicyService) Update(c *gin.Context) (*GetStoragePo
|
||||
}
|
||||
|
||||
service.Policy.ID = idInt
|
||||
_, err = storagePolicyClient.Upsert(c, service.Policy)
|
||||
|
||||
sc, tx, ctx, err := inventory.WithTx(c, storagePolicyClient)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to create transaction", err)
|
||||
}
|
||||
|
||||
_, err = sc.Upsert(ctx, service.Policy)
|
||||
if err != nil {
|
||||
_ = inventory.Rollback(tx)
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to update policy", err)
|
||||
}
|
||||
|
||||
if err := inventory.Commit(tx); err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to commit transaction", err)
|
||||
}
|
||||
|
||||
_ = dep.KV().Delete(manager.EntityUrlCacheKeyPrefix)
|
||||
|
||||
s := SingleStoragePolicyService{ID: idInt}
|
||||
|
||||
@@ -3,6 +3,7 @@ package admin
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
@@ -251,3 +252,31 @@ func (s *BatchTaskService) Delete(c *gin.Context) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type (
|
||||
CleanupTaskService struct {
|
||||
NotAfter time.Time `json:"not_after" binding:"required"`
|
||||
Types []string `json:"types"`
|
||||
Status []task.Status `json:"status"`
|
||||
}
|
||||
CleanupTaskParameterCtx struct{}
|
||||
)
|
||||
|
||||
func (s *CleanupTaskService) CleanupTask(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
taskClient := dep.TaskClient()
|
||||
|
||||
if len(s.Status) == 0 {
|
||||
s.Status = []task.Status{task.StatusCanceled, task.StatusCompleted, task.StatusError}
|
||||
}
|
||||
|
||||
if err := taskClient.DeleteBy(c, &inventory.DeleteTaskArgs{
|
||||
NotAfter: s.NotAfter,
|
||||
Types: s.Types,
|
||||
Status: s.Status,
|
||||
}); err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to cleanup tasks", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -13,13 +13,15 @@ import (
|
||||
// SiteConfig 站点全局设置序列
|
||||
type SiteConfig struct {
|
||||
// Basic Section
|
||||
InstanceID string `json:"instance_id,omitempty"`
|
||||
SiteName string `json:"title,omitempty"`
|
||||
Themes string `json:"themes,omitempty"`
|
||||
DefaultTheme string `json:"default_theme,omitempty"`
|
||||
User *user.User `json:"user,omitempty"`
|
||||
Logo string `json:"logo,omitempty"`
|
||||
LogoLight string `json:"logo_light,omitempty"`
|
||||
InstanceID string `json:"instance_id,omitempty"`
|
||||
SiteName string `json:"title,omitempty"`
|
||||
Themes string `json:"themes,omitempty"`
|
||||
DefaultTheme string `json:"default_theme,omitempty"`
|
||||
User *user.User `json:"user,omitempty"`
|
||||
Logo string `json:"logo,omitempty"`
|
||||
LogoLight string `json:"logo_light,omitempty"`
|
||||
CustomNavItems []setting.CustomNavItem `json:"custom_nav_items,omitempty"`
|
||||
CustomHTML *setting.CustomHTML `json:"custom_html,omitempty"`
|
||||
|
||||
// Login Section
|
||||
LoginCaptcha bool `json:"login_captcha,omitempty"`
|
||||
@@ -31,6 +33,7 @@ type SiteConfig struct {
|
||||
TurnstileSiteID string `json:"turnstile_site_id,omitempty"`
|
||||
CapInstanceURL string `json:"captcha_cap_instance_url,omitempty"`
|
||||
CapSiteKey string `json:"captcha_cap_site_key,omitempty"`
|
||||
CapAssetServer string `json:"captcha_cap_asset_server,omitempty"`
|
||||
RegisterEnabled bool `json:"register_enabled,omitempty"`
|
||||
TosUrl string `json:"tos_url,omitempty"`
|
||||
PrivacyPolicyUrl string `json:"privacy_policy_url,omitempty"`
|
||||
@@ -44,6 +47,7 @@ type SiteConfig struct {
|
||||
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"`
|
||||
|
||||
// App settings
|
||||
AppPromotion bool `json:"app_promotion,omitempty"`
|
||||
@@ -86,6 +90,7 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) {
|
||||
explorerSettings := settings.ExplorerFrontendSettings(c)
|
||||
mapSettings := settings.MapSetting(c)
|
||||
fileViewers := settings.FileViewers(c)
|
||||
customProps := settings.CustomProps(c)
|
||||
maxBatchSize := settings.MaxBatchedFile(c)
|
||||
w, h := settings.ThumbSize(c)
|
||||
for i := range fileViewers {
|
||||
@@ -101,6 +106,7 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) {
|
||||
GoogleMapTileType: mapSettings.GoogleTileType,
|
||||
ThumbnailWidth: w,
|
||||
ThumbnailHeight: h,
|
||||
CustomProps: customProps,
|
||||
}, nil
|
||||
case "emojis":
|
||||
emojis := settings.EmojiPresets(c)
|
||||
@@ -124,7 +130,8 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) {
|
||||
reCaptcha := settings.ReCaptcha(c)
|
||||
capCaptcha := settings.CapCaptcha(c)
|
||||
appSetting := settings.AppSetting(c)
|
||||
|
||||
customNavItems := settings.CustomNavItems(c)
|
||||
customHTML := settings.CustomHTML(c)
|
||||
return &SiteConfig{
|
||||
InstanceID: siteBasic.ID,
|
||||
SiteName: siteBasic.Name,
|
||||
@@ -138,7 +145,10 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) {
|
||||
ReCaptchaKey: reCaptcha.Key,
|
||||
CapInstanceURL: capCaptcha.InstanceURL,
|
||||
CapSiteKey: capCaptcha.SiteKey,
|
||||
CapAssetServer: capCaptcha.AssetServer,
|
||||
AppPromotion: appSetting.Promotion,
|
||||
CustomNavItems: customNavItems,
|
||||
CustomHTML: customHTML,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -659,7 +659,7 @@ func (s *GetFileInfoService) Get(c *gin.Context) (*FileResponse, error) {
|
||||
return BuildFileResponse(c, user, file, dep.HashIDEncoder(), nil), nil
|
||||
}
|
||||
|
||||
func RedirectDirectLink(c *gin.Context, name string) error {
|
||||
func RedirectDirectLink(c *gin.Context, name string, download bool) error {
|
||||
dep := dependency.FromContext(c)
|
||||
settings := dep.SettingProvider()
|
||||
|
||||
@@ -680,6 +680,7 @@ func RedirectDirectLink(c *gin.Context, name string) error {
|
||||
expire := time.Now().Add(settings.EntityUrlValidDuration(c))
|
||||
res, earliestExpire, err := m.GetUrlForRedirectedDirectLink(c, dl,
|
||||
fs.WithUrlExpire(&expire),
|
||||
fs.WithIsDownload(download),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -280,6 +280,7 @@ type Share struct {
|
||||
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"`
|
||||
@@ -313,6 +314,7 @@ func BuildShare(s *ent.Share, base *url.URL, hasher hashid.Encoder, requester *e
|
||||
res.Downloaded = s.Downloads
|
||||
res.Expires = s.Expires
|
||||
res.Password = s.Password
|
||||
res.ShowReadMe = s.Props != nil && s.Props.ShowReadMe
|
||||
}
|
||||
|
||||
if requester.ID == owner.ID {
|
||||
|
||||
@@ -24,6 +24,7 @@ type (
|
||||
RemainDownloads int `json:"downloads"`
|
||||
Expire int `json:"expire"`
|
||||
ShareView bool `json:"share_view"`
|
||||
ShowReadMe bool `json:"show_readme"`
|
||||
}
|
||||
ShareCreateParamCtx struct{}
|
||||
)
|
||||
@@ -58,6 +59,7 @@ func (service *ShareCreateService) Upsert(c *gin.Context, existed int) (string,
|
||||
Expire: expires,
|
||||
ExistedShareID: existed,
|
||||
ShareView: service.ShareView,
|
||||
ShowReadMe: service.ShowReadMe,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
Reference in New Issue
Block a user