Compare commits

...

20 Commits
4.1.1 ... 4.3.0

Author SHA1 Message Date
Aaron Liu
1cdccf5fc9 feat(thumb): adding option to define custom input argument for FFmpeg (#2657) 2025-07-15 14:11:42 +08:00
Aaron Liu
15762cb393 feat(thumb): support output webp thumbnails for vips generator (#2657) 2025-07-15 13:51:23 +08:00
Aaron Liu
e96b595622 feat(direct link): add option to get direct link with download enforced (#2651) 2025-07-15 13:22:04 +08:00
Aaron Liu
d19fc0e75c feat(remote download): sanitize file names with special characters (#2648) 2025-07-15 12:00:39 +08:00
Aaron Liu
195d68c535 chore(docker): add LibRAW into docker image (#2645) 2025-07-15 11:01:44 +08:00
Aaron Liu
000124f6c7 feat(ui): custom HTML content in predefined locations (#2621) 2025-07-15 10:45:32 +08:00
Aaron Liu
ca57ca1ba0 feat(custom): custom sidebar items 2025-07-15 10:41:13 +08:00
Aaron Liu
3cda4d1ef7 feat(fs): custom properties for files (#2407) 2025-07-12 11:15:33 +08:00
Aaron Liu
b13490357b feat(dashboard): cleanup tasks and events (#2368) 2025-07-05 11:52:15 +08:00
Aaron Liu
617d3a4262 feat(qiniu): use accelerated upload domain (#2497) 2025-07-05 10:50:51 +08:00
Aaron Liu
75a03aa708 fix(auth): unified empty path for sign content (#2616) 2025-07-05 10:05:09 +08:00
Aaron Liu
fe2ccb4d4e feat(share): add option to automatically render and show README file (#2382) 2025-07-04 14:40:32 +08:00
Aaron Liu
aada3aab02 feat(storage): load balance storage policy (#2436) 2025-07-04 10:05:15 +08:00
Samler
a0aefef691 feat: platform self-adaptation for file viewer application (#2603) 2025-07-03 14:04:14 +08:00
Aaron Liu
17fc598fb3 doc: duplicated OneDrive in README 2025-06-30 19:46:22 +08:00
Samler
19a65b065c fix: new user group error in without replication (#2596) 2025-06-30 19:34:18 +08:00
Anye
e0b2b4649e fix(db): map MariaDB type to MySQL (#2587)
* fix(db): 将MariaDB数据库类型映射到MySQL类型

* Update client.go
2025-06-30 19:32:21 +08:00
Aaron Liu
642c32c6cc chore: update fatih/color (#2591) 2025-06-29 10:48:25 +08:00
WittF
6106b57bc7 feat(captcha): update static asset source option (#2589)
* feat(captcha): Add captcha_cap_asset_server configuration option to support static asset server settings (#2584)

* fix(captcha): Backend default: cdn → jsdelivr
2025-06-29 10:14:26 +08:00
Aaron Liu
f38f32f9f5 fix(db): sslmode prefer not supported in some pg version (?) related: #2540 2025-06-27 13:54:10 +08:00
37 changed files with 491 additions and 84 deletions

View File

@@ -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

View File

@@ -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

Submodule assets updated: ac6f97d9ba...0b49582a07

View File

@@ -1,5 +1,5 @@
services:
pro:
cloudreve:
image: cloudreve/cloudreve:latest
container_name: cloudreve-backend
depends_on:

4
go.mod
View File

@@ -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
View File

@@ -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=

View File

@@ -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,

View File

@@ -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

View File

@@ -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))

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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"
)

View File

@@ -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
}

View File

@@ -10,6 +10,7 @@ var (
MySqlDB DBType = "mysql"
MsSqlDB DBType = "mssql"
PostgresDB DBType = "postgres"
MariaDB DBType = "mariadb"
)
// Database 数据库

View File

@@ -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,

View File

@@ -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
}
}

View File

@@ -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),
)

View File

@@ -115,6 +115,7 @@ type (
RemainDownloads int
Expire *time.Time
ShareView bool
ShowReadMe bool
}
)

View File

@@ -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")
},
},
}
)

View File

@@ -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{

View File

@@ -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)
}

View File

@@ -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"),
}
}

View File

@@ -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"`
}

View File

@@ -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

View File

@@ -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) }},

View File

@@ -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]"

View File

@@ -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)

View File

@@ -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
}
}
}

View File

@@ -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)
// // 新建文件导入任务

View File

@@ -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}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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