mirror of
https://github.com/cloudreve/cloudreve.git
synced 2026-03-09 14:07:00 +00:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f73583b370 | ||
|
|
c0132a10cb | ||
|
|
927c3bff00 | ||
|
|
bb9b42eb10 | ||
|
|
5f18d277c8 | ||
|
|
b0057fe92f | ||
|
|
bb3db2e326 | ||
|
|
8deeadb1e5 | ||
|
|
8688069fac | ||
|
|
4c08644b05 | ||
|
|
4c976b8627 | ||
|
|
b0375f5a24 | ||
|
|
48e9719336 | ||
|
|
7654ce889c | ||
|
|
80b25e88ee | ||
|
|
e31a6cbcb3 | ||
|
|
51d9e06f21 | ||
|
|
36be9b7a19 | ||
|
|
c8c2a60adb | ||
|
|
60bf0e02b3 | ||
|
|
488f32512d | ||
|
|
1cdccf5fc9 | ||
|
|
15762cb393 | ||
|
|
e96b595622 | ||
|
|
d19fc0e75c | ||
|
|
195d68c535 | ||
|
|
000124f6c7 | ||
|
|
ca57ca1ba0 | ||
|
|
3cda4d1ef7 |
@@ -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
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
Cloudreve
|
||||
<br>
|
||||
</h1>
|
||||
<h4 align="center">Self-hosted file management system with muilt-cloud support.</h4>
|
||||
<h4 align="center">Self-hosted file management system with multi-cloud support.</h4>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://dev.azure.com/abslantliu/Cloudreve/_build?definitionId=6">
|
||||
@@ -43,13 +43,13 @@
|
||||
- 💾 Integrate with Aria2/qBittorrent to download files in background, use multiple download nodes to share the load.
|
||||
- 📚 Compress/Extract files, download files in batch.
|
||||
- 💻 WebDAV support covering all storage providers.
|
||||
- :zap:Drag&Drop to upload files or folders, with resumeable upload support.
|
||||
- :zap:Drag&Drop to upload files or folders, with resumable upload support.
|
||||
- :card_file_box: Extract media metadata from files, search files by metadata or tags.
|
||||
- :family_woman_girl_boy: Multi-users with multi-groups.
|
||||
- :link: Create share links for files and folders with expiration date.
|
||||
- :eye_speech_bubble: Preview videos, images, audios, ePub files online; edit texts, diagrams, Markdown, images, Office documents online.
|
||||
- :art: Customize theme colors, dark mode, PWA application, SPA, i18n.
|
||||
- :rocket: All-In-One packing, with all features out-of-the-box.
|
||||
- :rocket: All-in-one packaging, with all features out of the box.
|
||||
- 🌈 ... ...
|
||||
|
||||
## :hammer_and_wrench: Deploy
|
||||
|
||||
@@ -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 跨域配置
|
||||
|
||||
2
assets
2
assets
Submodule assets updated: e9b91c4e03...f7aa0a09e2
@@ -1,5 +1,5 @@
|
||||
services:
|
||||
pro:
|
||||
cloudreve:
|
||||
image: cloudreve/cloudreve:latest
|
||||
container_name: cloudreve-backend
|
||||
depends_on:
|
||||
@@ -19,7 +19,10 @@ services:
|
||||
- backend_data:/cloudreve/data
|
||||
|
||||
postgresql:
|
||||
image: postgres:latest
|
||||
# Best practice: Pin to major version.
|
||||
# NOTE: For major version jumps:
|
||||
# backup & consult https://www.postgresql.org/docs/current/pgupgrade.html
|
||||
image: postgres:17
|
||||
container_name: postgresql
|
||||
environment:
|
||||
- POSTGRES_USER=cloudreve
|
||||
|
||||
@@ -1034,8 +1034,7 @@ func (c *FileClient) Hooks() []Hook {
|
||||
|
||||
// Interceptors returns the client interceptors.
|
||||
func (c *FileClient) Interceptors() []Interceptor {
|
||||
inters := c.inters.File
|
||||
return append(inters[:len(inters):len(inters)], file.Interceptors[:]...)
|
||||
return c.inters.File
|
||||
}
|
||||
|
||||
func (c *FileClient) mutate(ctx context.Context, m *FileMutation) (Value, error) {
|
||||
|
||||
16
ent/file.go
16
ent/file.go
@@ -25,8 +25,6 @@ type File struct {
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
// UpdatedAt holds the value of the "updated_at" field.
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
// DeletedAt holds the value of the "deleted_at" field.
|
||||
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
||||
// Type holds the value of the "type" field.
|
||||
Type int `json:"type,omitempty"`
|
||||
// Name holds the value of the "name" field.
|
||||
@@ -171,7 +169,7 @@ func (*File) scanValues(columns []string) ([]any, error) {
|
||||
values[i] = new(sql.NullInt64)
|
||||
case file.FieldName:
|
||||
values[i] = new(sql.NullString)
|
||||
case file.FieldCreatedAt, file.FieldUpdatedAt, file.FieldDeletedAt:
|
||||
case file.FieldCreatedAt, file.FieldUpdatedAt:
|
||||
values[i] = new(sql.NullTime)
|
||||
default:
|
||||
values[i] = new(sql.UnknownType)
|
||||
@@ -206,13 +204,6 @@ func (f *File) assignValues(columns []string, values []any) error {
|
||||
} else if value.Valid {
|
||||
f.UpdatedAt = value.Time
|
||||
}
|
||||
case file.FieldDeletedAt:
|
||||
if value, ok := values[i].(*sql.NullTime); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field deleted_at", values[i])
|
||||
} else if value.Valid {
|
||||
f.DeletedAt = new(time.Time)
|
||||
*f.DeletedAt = value.Time
|
||||
}
|
||||
case file.FieldType:
|
||||
if value, ok := values[i].(*sql.NullInt64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field type", values[i])
|
||||
@@ -351,11 +342,6 @@ func (f *File) String() string {
|
||||
builder.WriteString("updated_at=")
|
||||
builder.WriteString(f.UpdatedAt.Format(time.ANSIC))
|
||||
builder.WriteString(", ")
|
||||
if v := f.DeletedAt; v != nil {
|
||||
builder.WriteString("deleted_at=")
|
||||
builder.WriteString(v.Format(time.ANSIC))
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("type=")
|
||||
builder.WriteString(fmt.Sprintf("%v", f.Type))
|
||||
builder.WriteString(", ")
|
||||
|
||||
@@ -19,8 +19,6 @@ const (
|
||||
FieldCreatedAt = "created_at"
|
||||
// FieldUpdatedAt holds the string denoting the updated_at field in the database.
|
||||
FieldUpdatedAt = "updated_at"
|
||||
// FieldDeletedAt holds the string denoting the deleted_at field in the database.
|
||||
FieldDeletedAt = "deleted_at"
|
||||
// FieldType holds the string denoting the type field in the database.
|
||||
FieldType = "type"
|
||||
// FieldName holds the string denoting the name field in the database.
|
||||
@@ -112,7 +110,6 @@ var Columns = []string{
|
||||
FieldID,
|
||||
FieldCreatedAt,
|
||||
FieldUpdatedAt,
|
||||
FieldDeletedAt,
|
||||
FieldType,
|
||||
FieldName,
|
||||
FieldOwnerID,
|
||||
@@ -146,14 +143,11 @@ func ValidColumn(column string) bool {
|
||||
//
|
||||
// import _ "github.com/cloudreve/Cloudreve/v4/ent/runtime"
|
||||
var (
|
||||
Hooks [1]ent.Hook
|
||||
Interceptors [1]ent.Interceptor
|
||||
Hooks [1]ent.Hook
|
||||
// DefaultCreatedAt holds the default value on creation for the "created_at" field.
|
||||
DefaultCreatedAt func() time.Time
|
||||
// DefaultUpdatedAt holds the default value on creation for the "updated_at" field.
|
||||
DefaultUpdatedAt func() time.Time
|
||||
// UpdateDefaultUpdatedAt holds the default value on update for the "updated_at" field.
|
||||
UpdateDefaultUpdatedAt func() time.Time
|
||||
// DefaultSize holds the default value on creation for the "size" field.
|
||||
DefaultSize int64
|
||||
// DefaultIsSymbolic holds the default value on creation for the "is_symbolic" field.
|
||||
@@ -178,11 +172,6 @@ func ByUpdatedAt(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldUpdatedAt, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByDeletedAt orders the results by the deleted_at field.
|
||||
func ByDeletedAt(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldDeletedAt, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByType orders the results by the type field.
|
||||
func ByType(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldType, opts...).ToFunc()
|
||||
|
||||
@@ -65,11 +65,6 @@ func UpdatedAt(v time.Time) predicate.File {
|
||||
return predicate.File(sql.FieldEQ(FieldUpdatedAt, v))
|
||||
}
|
||||
|
||||
// DeletedAt applies equality check predicate on the "deleted_at" field. It's identical to DeletedAtEQ.
|
||||
func DeletedAt(v time.Time) predicate.File {
|
||||
return predicate.File(sql.FieldEQ(FieldDeletedAt, v))
|
||||
}
|
||||
|
||||
// Type applies equality check predicate on the "type" field. It's identical to TypeEQ.
|
||||
func Type(v int) predicate.File {
|
||||
return predicate.File(sql.FieldEQ(FieldType, v))
|
||||
@@ -190,56 +185,6 @@ func UpdatedAtLTE(v time.Time) predicate.File {
|
||||
return predicate.File(sql.FieldLTE(FieldUpdatedAt, v))
|
||||
}
|
||||
|
||||
// DeletedAtEQ applies the EQ predicate on the "deleted_at" field.
|
||||
func DeletedAtEQ(v time.Time) predicate.File {
|
||||
return predicate.File(sql.FieldEQ(FieldDeletedAt, v))
|
||||
}
|
||||
|
||||
// DeletedAtNEQ applies the NEQ predicate on the "deleted_at" field.
|
||||
func DeletedAtNEQ(v time.Time) predicate.File {
|
||||
return predicate.File(sql.FieldNEQ(FieldDeletedAt, v))
|
||||
}
|
||||
|
||||
// DeletedAtIn applies the In predicate on the "deleted_at" field.
|
||||
func DeletedAtIn(vs ...time.Time) predicate.File {
|
||||
return predicate.File(sql.FieldIn(FieldDeletedAt, vs...))
|
||||
}
|
||||
|
||||
// DeletedAtNotIn applies the NotIn predicate on the "deleted_at" field.
|
||||
func DeletedAtNotIn(vs ...time.Time) predicate.File {
|
||||
return predicate.File(sql.FieldNotIn(FieldDeletedAt, vs...))
|
||||
}
|
||||
|
||||
// DeletedAtGT applies the GT predicate on the "deleted_at" field.
|
||||
func DeletedAtGT(v time.Time) predicate.File {
|
||||
return predicate.File(sql.FieldGT(FieldDeletedAt, v))
|
||||
}
|
||||
|
||||
// DeletedAtGTE applies the GTE predicate on the "deleted_at" field.
|
||||
func DeletedAtGTE(v time.Time) predicate.File {
|
||||
return predicate.File(sql.FieldGTE(FieldDeletedAt, v))
|
||||
}
|
||||
|
||||
// DeletedAtLT applies the LT predicate on the "deleted_at" field.
|
||||
func DeletedAtLT(v time.Time) predicate.File {
|
||||
return predicate.File(sql.FieldLT(FieldDeletedAt, v))
|
||||
}
|
||||
|
||||
// DeletedAtLTE applies the LTE predicate on the "deleted_at" field.
|
||||
func DeletedAtLTE(v time.Time) predicate.File {
|
||||
return predicate.File(sql.FieldLTE(FieldDeletedAt, v))
|
||||
}
|
||||
|
||||
// DeletedAtIsNil applies the IsNil predicate on the "deleted_at" field.
|
||||
func DeletedAtIsNil() predicate.File {
|
||||
return predicate.File(sql.FieldIsNull(FieldDeletedAt))
|
||||
}
|
||||
|
||||
// DeletedAtNotNil applies the NotNil predicate on the "deleted_at" field.
|
||||
func DeletedAtNotNil() predicate.File {
|
||||
return predicate.File(sql.FieldNotNull(FieldDeletedAt))
|
||||
}
|
||||
|
||||
// TypeEQ applies the EQ predicate on the "type" field.
|
||||
func TypeEQ(v int) predicate.File {
|
||||
return predicate.File(sql.FieldEQ(FieldType, v))
|
||||
|
||||
@@ -57,20 +57,6 @@ func (fc *FileCreate) SetNillableUpdatedAt(t *time.Time) *FileCreate {
|
||||
return fc
|
||||
}
|
||||
|
||||
// SetDeletedAt sets the "deleted_at" field.
|
||||
func (fc *FileCreate) SetDeletedAt(t time.Time) *FileCreate {
|
||||
fc.mutation.SetDeletedAt(t)
|
||||
return fc
|
||||
}
|
||||
|
||||
// SetNillableDeletedAt sets the "deleted_at" field if the given value is not nil.
|
||||
func (fc *FileCreate) SetNillableDeletedAt(t *time.Time) *FileCreate {
|
||||
if t != nil {
|
||||
fc.SetDeletedAt(*t)
|
||||
}
|
||||
return fc
|
||||
}
|
||||
|
||||
// SetType sets the "type" field.
|
||||
func (fc *FileCreate) SetType(i int) *FileCreate {
|
||||
fc.mutation.SetType(i)
|
||||
@@ -413,10 +399,6 @@ func (fc *FileCreate) createSpec() (*File, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(file.FieldUpdatedAt, field.TypeTime, value)
|
||||
_node.UpdatedAt = value
|
||||
}
|
||||
if value, ok := fc.mutation.DeletedAt(); ok {
|
||||
_spec.SetField(file.FieldDeletedAt, field.TypeTime, value)
|
||||
_node.DeletedAt = &value
|
||||
}
|
||||
if value, ok := fc.mutation.GetType(); ok {
|
||||
_spec.SetField(file.FieldType, field.TypeInt, value)
|
||||
_node.Type = value
|
||||
@@ -636,24 +618,6 @@ func (u *FileUpsert) UpdateUpdatedAt() *FileUpsert {
|
||||
return u
|
||||
}
|
||||
|
||||
// SetDeletedAt sets the "deleted_at" field.
|
||||
func (u *FileUpsert) SetDeletedAt(v time.Time) *FileUpsert {
|
||||
u.Set(file.FieldDeletedAt, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateDeletedAt sets the "deleted_at" field to the value that was provided on create.
|
||||
func (u *FileUpsert) UpdateDeletedAt() *FileUpsert {
|
||||
u.SetExcluded(file.FieldDeletedAt)
|
||||
return u
|
||||
}
|
||||
|
||||
// ClearDeletedAt clears the value of the "deleted_at" field.
|
||||
func (u *FileUpsert) ClearDeletedAt() *FileUpsert {
|
||||
u.SetNull(file.FieldDeletedAt)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetType sets the "type" field.
|
||||
func (u *FileUpsert) SetType(v int) *FileUpsert {
|
||||
u.Set(file.FieldType, v)
|
||||
@@ -863,27 +827,6 @@ func (u *FileUpsertOne) UpdateUpdatedAt() *FileUpsertOne {
|
||||
})
|
||||
}
|
||||
|
||||
// SetDeletedAt sets the "deleted_at" field.
|
||||
func (u *FileUpsertOne) SetDeletedAt(v time.Time) *FileUpsertOne {
|
||||
return u.Update(func(s *FileUpsert) {
|
||||
s.SetDeletedAt(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateDeletedAt sets the "deleted_at" field to the value that was provided on create.
|
||||
func (u *FileUpsertOne) UpdateDeletedAt() *FileUpsertOne {
|
||||
return u.Update(func(s *FileUpsert) {
|
||||
s.UpdateDeletedAt()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearDeletedAt clears the value of the "deleted_at" field.
|
||||
func (u *FileUpsertOne) ClearDeletedAt() *FileUpsertOne {
|
||||
return u.Update(func(s *FileUpsert) {
|
||||
s.ClearDeletedAt()
|
||||
})
|
||||
}
|
||||
|
||||
// SetType sets the "type" field.
|
||||
func (u *FileUpsertOne) SetType(v int) *FileUpsertOne {
|
||||
return u.Update(func(s *FileUpsert) {
|
||||
@@ -1289,27 +1232,6 @@ func (u *FileUpsertBulk) UpdateUpdatedAt() *FileUpsertBulk {
|
||||
})
|
||||
}
|
||||
|
||||
// SetDeletedAt sets the "deleted_at" field.
|
||||
func (u *FileUpsertBulk) SetDeletedAt(v time.Time) *FileUpsertBulk {
|
||||
return u.Update(func(s *FileUpsert) {
|
||||
s.SetDeletedAt(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateDeletedAt sets the "deleted_at" field to the value that was provided on create.
|
||||
func (u *FileUpsertBulk) UpdateDeletedAt() *FileUpsertBulk {
|
||||
return u.Update(func(s *FileUpsert) {
|
||||
s.UpdateDeletedAt()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearDeletedAt clears the value of the "deleted_at" field.
|
||||
func (u *FileUpsertBulk) ClearDeletedAt() *FileUpsertBulk {
|
||||
return u.Update(func(s *FileUpsert) {
|
||||
s.ClearDeletedAt()
|
||||
})
|
||||
}
|
||||
|
||||
// SetType sets the "type" field.
|
||||
func (u *FileUpsertBulk) SetType(v int) *FileUpsertBulk {
|
||||
return u.Update(func(s *FileUpsert) {
|
||||
|
||||
@@ -41,26 +41,14 @@ func (fu *FileUpdate) SetUpdatedAt(t time.Time) *FileUpdate {
|
||||
return fu
|
||||
}
|
||||
|
||||
// SetDeletedAt sets the "deleted_at" field.
|
||||
func (fu *FileUpdate) SetDeletedAt(t time.Time) *FileUpdate {
|
||||
fu.mutation.SetDeletedAt(t)
|
||||
return fu
|
||||
}
|
||||
|
||||
// SetNillableDeletedAt sets the "deleted_at" field if the given value is not nil.
|
||||
func (fu *FileUpdate) SetNillableDeletedAt(t *time.Time) *FileUpdate {
|
||||
// SetNillableUpdatedAt sets the "updated_at" field if the given value is not nil.
|
||||
func (fu *FileUpdate) SetNillableUpdatedAt(t *time.Time) *FileUpdate {
|
||||
if t != nil {
|
||||
fu.SetDeletedAt(*t)
|
||||
fu.SetUpdatedAt(*t)
|
||||
}
|
||||
return fu
|
||||
}
|
||||
|
||||
// ClearDeletedAt clears the value of the "deleted_at" field.
|
||||
func (fu *FileUpdate) ClearDeletedAt() *FileUpdate {
|
||||
fu.mutation.ClearDeletedAt()
|
||||
return fu
|
||||
}
|
||||
|
||||
// SetType sets the "type" field.
|
||||
func (fu *FileUpdate) SetType(i int) *FileUpdate {
|
||||
fu.mutation.ResetType()
|
||||
@@ -472,9 +460,6 @@ func (fu *FileUpdate) RemoveDirectLinks(d ...*DirectLink) *FileUpdate {
|
||||
|
||||
// Save executes the query and returns the number of nodes affected by the update operation.
|
||||
func (fu *FileUpdate) Save(ctx context.Context) (int, error) {
|
||||
if err := fu.defaults(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return withHooks(ctx, fu.sqlSave, fu.mutation, fu.hooks)
|
||||
}
|
||||
|
||||
@@ -500,18 +485,6 @@ func (fu *FileUpdate) ExecX(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// defaults sets the default values of the builder before save.
|
||||
func (fu *FileUpdate) defaults() error {
|
||||
if _, ok := fu.mutation.UpdatedAt(); !ok {
|
||||
if file.UpdateDefaultUpdatedAt == nil {
|
||||
return fmt.Errorf("ent: uninitialized file.UpdateDefaultUpdatedAt (forgotten import ent/runtime?)")
|
||||
}
|
||||
v := file.UpdateDefaultUpdatedAt()
|
||||
fu.mutation.SetUpdatedAt(v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// check runs all checks and user-defined validators on the builder.
|
||||
func (fu *FileUpdate) check() error {
|
||||
if _, ok := fu.mutation.OwnerID(); fu.mutation.OwnerCleared() && !ok {
|
||||
@@ -535,12 +508,6 @@ func (fu *FileUpdate) sqlSave(ctx context.Context) (n int, err error) {
|
||||
if value, ok := fu.mutation.UpdatedAt(); ok {
|
||||
_spec.SetField(file.FieldUpdatedAt, field.TypeTime, value)
|
||||
}
|
||||
if value, ok := fu.mutation.DeletedAt(); ok {
|
||||
_spec.SetField(file.FieldDeletedAt, field.TypeTime, value)
|
||||
}
|
||||
if fu.mutation.DeletedAtCleared() {
|
||||
_spec.ClearField(file.FieldDeletedAt, field.TypeTime)
|
||||
}
|
||||
if value, ok := fu.mutation.GetType(); ok {
|
||||
_spec.SetField(file.FieldType, field.TypeInt, value)
|
||||
}
|
||||
@@ -912,26 +879,14 @@ func (fuo *FileUpdateOne) SetUpdatedAt(t time.Time) *FileUpdateOne {
|
||||
return fuo
|
||||
}
|
||||
|
||||
// SetDeletedAt sets the "deleted_at" field.
|
||||
func (fuo *FileUpdateOne) SetDeletedAt(t time.Time) *FileUpdateOne {
|
||||
fuo.mutation.SetDeletedAt(t)
|
||||
return fuo
|
||||
}
|
||||
|
||||
// SetNillableDeletedAt sets the "deleted_at" field if the given value is not nil.
|
||||
func (fuo *FileUpdateOne) SetNillableDeletedAt(t *time.Time) *FileUpdateOne {
|
||||
// SetNillableUpdatedAt sets the "updated_at" field if the given value is not nil.
|
||||
func (fuo *FileUpdateOne) SetNillableUpdatedAt(t *time.Time) *FileUpdateOne {
|
||||
if t != nil {
|
||||
fuo.SetDeletedAt(*t)
|
||||
fuo.SetUpdatedAt(*t)
|
||||
}
|
||||
return fuo
|
||||
}
|
||||
|
||||
// ClearDeletedAt clears the value of the "deleted_at" field.
|
||||
func (fuo *FileUpdateOne) ClearDeletedAt() *FileUpdateOne {
|
||||
fuo.mutation.ClearDeletedAt()
|
||||
return fuo
|
||||
}
|
||||
|
||||
// SetType sets the "type" field.
|
||||
func (fuo *FileUpdateOne) SetType(i int) *FileUpdateOne {
|
||||
fuo.mutation.ResetType()
|
||||
@@ -1356,9 +1311,6 @@ func (fuo *FileUpdateOne) Select(field string, fields ...string) *FileUpdateOne
|
||||
|
||||
// Save executes the query and returns the updated File entity.
|
||||
func (fuo *FileUpdateOne) Save(ctx context.Context) (*File, error) {
|
||||
if err := fuo.defaults(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return withHooks(ctx, fuo.sqlSave, fuo.mutation, fuo.hooks)
|
||||
}
|
||||
|
||||
@@ -1384,18 +1336,6 @@ func (fuo *FileUpdateOne) ExecX(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// defaults sets the default values of the builder before save.
|
||||
func (fuo *FileUpdateOne) defaults() error {
|
||||
if _, ok := fuo.mutation.UpdatedAt(); !ok {
|
||||
if file.UpdateDefaultUpdatedAt == nil {
|
||||
return fmt.Errorf("ent: uninitialized file.UpdateDefaultUpdatedAt (forgotten import ent/runtime?)")
|
||||
}
|
||||
v := file.UpdateDefaultUpdatedAt()
|
||||
fuo.mutation.SetUpdatedAt(v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// check runs all checks and user-defined validators on the builder.
|
||||
func (fuo *FileUpdateOne) check() error {
|
||||
if _, ok := fuo.mutation.OwnerID(); fuo.mutation.OwnerCleared() && !ok {
|
||||
@@ -1436,12 +1376,6 @@ func (fuo *FileUpdateOne) sqlSave(ctx context.Context) (_node *File, err error)
|
||||
if value, ok := fuo.mutation.UpdatedAt(); ok {
|
||||
_spec.SetField(file.FieldUpdatedAt, field.TypeTime, value)
|
||||
}
|
||||
if value, ok := fuo.mutation.DeletedAt(); ok {
|
||||
_spec.SetField(file.FieldDeletedAt, field.TypeTime, value)
|
||||
}
|
||||
if fuo.mutation.DeletedAtCleared() {
|
||||
_spec.ClearField(file.FieldDeletedAt, field.TypeTime)
|
||||
}
|
||||
if value, ok := fuo.mutation.GetType(); ok {
|
||||
_spec.SetField(file.FieldType, field.TypeInt, value)
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -107,7 +107,6 @@ var (
|
||||
{Name: "id", Type: field.TypeInt, Increment: true},
|
||||
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"mysql": "datetime"}},
|
||||
{Name: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"mysql": "datetime"}},
|
||||
{Name: "deleted_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"mysql": "datetime"}},
|
||||
{Name: "type", Type: field.TypeInt},
|
||||
{Name: "name", Type: field.TypeString},
|
||||
{Name: "size", Type: field.TypeInt64, Default: 0},
|
||||
@@ -126,19 +125,19 @@ var (
|
||||
ForeignKeys: []*schema.ForeignKey{
|
||||
{
|
||||
Symbol: "files_files_children",
|
||||
Columns: []*schema.Column{FilesColumns[10]},
|
||||
Columns: []*schema.Column{FilesColumns[9]},
|
||||
RefColumns: []*schema.Column{FilesColumns[0]},
|
||||
OnDelete: schema.SetNull,
|
||||
},
|
||||
{
|
||||
Symbol: "files_storage_policies_files",
|
||||
Columns: []*schema.Column{FilesColumns[11]},
|
||||
Columns: []*schema.Column{FilesColumns[10]},
|
||||
RefColumns: []*schema.Column{StoragePoliciesColumns[0]},
|
||||
OnDelete: schema.SetNull,
|
||||
},
|
||||
{
|
||||
Symbol: "files_users_files",
|
||||
Columns: []*schema.Column{FilesColumns[12]},
|
||||
Columns: []*schema.Column{FilesColumns[11]},
|
||||
RefColumns: []*schema.Column{UsersColumns[0]},
|
||||
OnDelete: schema.NoAction,
|
||||
},
|
||||
@@ -147,17 +146,17 @@ var (
|
||||
{
|
||||
Name: "file_file_children_name",
|
||||
Unique: true,
|
||||
Columns: []*schema.Column{FilesColumns[10], FilesColumns[5]},
|
||||
Columns: []*schema.Column{FilesColumns[9], FilesColumns[4]},
|
||||
},
|
||||
{
|
||||
Name: "file_file_children_type_updated_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{FilesColumns[10], FilesColumns[4], FilesColumns[2]},
|
||||
Columns: []*schema.Column{FilesColumns[9], FilesColumns[3], FilesColumns[2]},
|
||||
},
|
||||
{
|
||||
Name: "file_file_children_type_size",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{FilesColumns[10], FilesColumns[4], FilesColumns[6]},
|
||||
Columns: []*schema.Column{FilesColumns[9], FilesColumns[3], FilesColumns[5]},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2972,7 +2972,6 @@ type FileMutation struct {
|
||||
id *int
|
||||
created_at *time.Time
|
||||
updated_at *time.Time
|
||||
deleted_at *time.Time
|
||||
_type *int
|
||||
add_type *int
|
||||
name *string
|
||||
@@ -3179,55 +3178,6 @@ func (m *FileMutation) ResetUpdatedAt() {
|
||||
m.updated_at = nil
|
||||
}
|
||||
|
||||
// SetDeletedAt sets the "deleted_at" field.
|
||||
func (m *FileMutation) SetDeletedAt(t time.Time) {
|
||||
m.deleted_at = &t
|
||||
}
|
||||
|
||||
// DeletedAt returns the value of the "deleted_at" field in the mutation.
|
||||
func (m *FileMutation) DeletedAt() (r time.Time, exists bool) {
|
||||
v := m.deleted_at
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldDeletedAt returns the old "deleted_at" field's value of the File entity.
|
||||
// If the File object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *FileMutation) OldDeletedAt(ctx context.Context) (v *time.Time, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldDeletedAt is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldDeletedAt requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldDeletedAt: %w", err)
|
||||
}
|
||||
return oldValue.DeletedAt, nil
|
||||
}
|
||||
|
||||
// ClearDeletedAt clears the value of the "deleted_at" field.
|
||||
func (m *FileMutation) ClearDeletedAt() {
|
||||
m.deleted_at = nil
|
||||
m.clearedFields[file.FieldDeletedAt] = struct{}{}
|
||||
}
|
||||
|
||||
// DeletedAtCleared returns if the "deleted_at" field was cleared in this mutation.
|
||||
func (m *FileMutation) DeletedAtCleared() bool {
|
||||
_, ok := m.clearedFields[file.FieldDeletedAt]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ResetDeletedAt resets all changes to the "deleted_at" field.
|
||||
func (m *FileMutation) ResetDeletedAt() {
|
||||
m.deleted_at = nil
|
||||
delete(m.clearedFields, file.FieldDeletedAt)
|
||||
}
|
||||
|
||||
// SetType sets the "type" field.
|
||||
func (m *FileMutation) SetType(i int) {
|
||||
m._type = &i
|
||||
@@ -4076,16 +4026,13 @@ func (m *FileMutation) Type() string {
|
||||
// order to get all numeric fields that were incremented/decremented, call
|
||||
// AddedFields().
|
||||
func (m *FileMutation) Fields() []string {
|
||||
fields := make([]string, 0, 12)
|
||||
fields := make([]string, 0, 11)
|
||||
if m.created_at != nil {
|
||||
fields = append(fields, file.FieldCreatedAt)
|
||||
}
|
||||
if m.updated_at != nil {
|
||||
fields = append(fields, file.FieldUpdatedAt)
|
||||
}
|
||||
if m.deleted_at != nil {
|
||||
fields = append(fields, file.FieldDeletedAt)
|
||||
}
|
||||
if m._type != nil {
|
||||
fields = append(fields, file.FieldType)
|
||||
}
|
||||
@@ -4125,8 +4072,6 @@ func (m *FileMutation) Field(name string) (ent.Value, bool) {
|
||||
return m.CreatedAt()
|
||||
case file.FieldUpdatedAt:
|
||||
return m.UpdatedAt()
|
||||
case file.FieldDeletedAt:
|
||||
return m.DeletedAt()
|
||||
case file.FieldType:
|
||||
return m.GetType()
|
||||
case file.FieldName:
|
||||
@@ -4158,8 +4103,6 @@ func (m *FileMutation) OldField(ctx context.Context, name string) (ent.Value, er
|
||||
return m.OldCreatedAt(ctx)
|
||||
case file.FieldUpdatedAt:
|
||||
return m.OldUpdatedAt(ctx)
|
||||
case file.FieldDeletedAt:
|
||||
return m.OldDeletedAt(ctx)
|
||||
case file.FieldType:
|
||||
return m.OldType(ctx)
|
||||
case file.FieldName:
|
||||
@@ -4201,13 +4144,6 @@ func (m *FileMutation) SetField(name string, value ent.Value) error {
|
||||
}
|
||||
m.SetUpdatedAt(v)
|
||||
return nil
|
||||
case file.FieldDeletedAt:
|
||||
v, ok := value.(time.Time)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetDeletedAt(v)
|
||||
return nil
|
||||
case file.FieldType:
|
||||
v, ok := value.(int)
|
||||
if !ok {
|
||||
@@ -4340,9 +4276,6 @@ func (m *FileMutation) AddField(name string, value ent.Value) error {
|
||||
// mutation.
|
||||
func (m *FileMutation) ClearedFields() []string {
|
||||
var fields []string
|
||||
if m.FieldCleared(file.FieldDeletedAt) {
|
||||
fields = append(fields, file.FieldDeletedAt)
|
||||
}
|
||||
if m.FieldCleared(file.FieldPrimaryEntity) {
|
||||
fields = append(fields, file.FieldPrimaryEntity)
|
||||
}
|
||||
@@ -4369,9 +4302,6 @@ func (m *FileMutation) FieldCleared(name string) bool {
|
||||
// error if the field is not defined in the schema.
|
||||
func (m *FileMutation) ClearField(name string) error {
|
||||
switch name {
|
||||
case file.FieldDeletedAt:
|
||||
m.ClearDeletedAt()
|
||||
return nil
|
||||
case file.FieldPrimaryEntity:
|
||||
m.ClearPrimaryEntity()
|
||||
return nil
|
||||
@@ -4398,9 +4328,6 @@ func (m *FileMutation) ResetField(name string) error {
|
||||
case file.FieldUpdatedAt:
|
||||
m.ResetUpdatedAt()
|
||||
return nil
|
||||
case file.FieldDeletedAt:
|
||||
m.ResetDeletedAt()
|
||||
return nil
|
||||
case file.FieldType:
|
||||
m.ResetType()
|
||||
return nil
|
||||
|
||||
@@ -87,31 +87,24 @@ func init() {
|
||||
entityDescReferenceCount := entityFields[3].Descriptor()
|
||||
// entity.DefaultReferenceCount holds the default value on creation for the reference_count field.
|
||||
entity.DefaultReferenceCount = entityDescReferenceCount.Default.(int)
|
||||
fileMixin := schema.File{}.Mixin()
|
||||
fileMixinHooks0 := fileMixin[0].Hooks()
|
||||
file.Hooks[0] = fileMixinHooks0[0]
|
||||
fileMixinInters0 := fileMixin[0].Interceptors()
|
||||
file.Interceptors[0] = fileMixinInters0[0]
|
||||
fileMixinFields0 := fileMixin[0].Fields()
|
||||
_ = fileMixinFields0
|
||||
fileHooks := schema.File{}.Hooks()
|
||||
file.Hooks[0] = fileHooks[0]
|
||||
fileFields := schema.File{}.Fields()
|
||||
_ = fileFields
|
||||
// fileDescCreatedAt is the schema descriptor for created_at field.
|
||||
fileDescCreatedAt := fileMixinFields0[0].Descriptor()
|
||||
fileDescCreatedAt := fileFields[0].Descriptor()
|
||||
// file.DefaultCreatedAt holds the default value on creation for the created_at field.
|
||||
file.DefaultCreatedAt = fileDescCreatedAt.Default.(func() time.Time)
|
||||
// fileDescUpdatedAt is the schema descriptor for updated_at field.
|
||||
fileDescUpdatedAt := fileMixinFields0[1].Descriptor()
|
||||
fileDescUpdatedAt := fileFields[1].Descriptor()
|
||||
// file.DefaultUpdatedAt holds the default value on creation for the updated_at field.
|
||||
file.DefaultUpdatedAt = fileDescUpdatedAt.Default.(func() time.Time)
|
||||
// file.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field.
|
||||
file.UpdateDefaultUpdatedAt = fileDescUpdatedAt.UpdateDefault.(func() time.Time)
|
||||
// fileDescSize is the schema descriptor for size field.
|
||||
fileDescSize := fileFields[3].Descriptor()
|
||||
fileDescSize := fileFields[5].Descriptor()
|
||||
// file.DefaultSize holds the default value on creation for the size field.
|
||||
file.DefaultSize = fileDescSize.Default.(int64)
|
||||
// fileDescIsSymbolic is the schema descriptor for is_symbolic field.
|
||||
fileDescIsSymbolic := fileFields[6].Descriptor()
|
||||
fileDescIsSymbolic := fileFields[8].Descriptor()
|
||||
// file.DefaultIsSymbolic holds the default value on creation for the is_symbolic field.
|
||||
file.DefaultIsSymbolic = fileDescIsSymbolic.Default.(bool)
|
||||
groupMixin := schema.Group{}.Mixin()
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect"
|
||||
"entgo.io/ent/schema/edge"
|
||||
"entgo.io/ent/schema/field"
|
||||
"entgo.io/ent/schema/index"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent/hook"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
)
|
||||
|
||||
@@ -16,6 +21,17 @@ type File struct {
|
||||
// Fields of the File.
|
||||
func (File) Fields() []ent.Field {
|
||||
return []ent.Field{
|
||||
field.Time("created_at").
|
||||
Immutable().
|
||||
Default(time.Now).
|
||||
SchemaType(map[string]string{
|
||||
dialect.MySQL: "datetime",
|
||||
}),
|
||||
field.Time("updated_at").
|
||||
Default(time.Now).
|
||||
SchemaType(map[string]string{
|
||||
dialect.MySQL: "datetime",
|
||||
}),
|
||||
field.Int("type"),
|
||||
field.String("name"),
|
||||
field.Int("owner_id"),
|
||||
@@ -66,8 +82,19 @@ func (File) Indexes() []ent.Index {
|
||||
}
|
||||
}
|
||||
|
||||
func (File) Mixin() []ent.Mixin {
|
||||
return []ent.Mixin{
|
||||
CommonMixin{},
|
||||
func (f File) Hooks() []ent.Hook {
|
||||
return []ent.Hook{
|
||||
hook.On(func(next ent.Mutator) ent.Mutator {
|
||||
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
|
||||
if s, ok := m.(interface{ SetUpdatedAt(time.Time) }); ok {
|
||||
_, set := m.Field("updated_at")
|
||||
if !set {
|
||||
s.SetUpdatedAt(time.Now())
|
||||
}
|
||||
}
|
||||
v, err := next.Mutate(ctx, m)
|
||||
return v, err
|
||||
})
|
||||
}, ent.OpUpdate|ent.OpUpdateOne),
|
||||
}
|
||||
}
|
||||
|
||||
5
go.mod
5
go.mod
@@ -22,7 +22,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
|
||||
@@ -38,6 +37,7 @@ require (
|
||||
github.com/jinzhu/gorm v1.9.11
|
||||
github.com/jpillora/backoff v1.0.0
|
||||
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/mojocn/base64Captcha v0.0.0-20190801020520-752b1cd608b2
|
||||
@@ -53,6 +53,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
|
||||
@@ -138,8 +139,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
|
||||
|
||||
52
go.sum
52
go.sum
@@ -274,7 +274,6 @@ 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=
|
||||
@@ -326,8 +325,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=
|
||||
@@ -636,6 +633,8 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/ks3sdklib/aws-sdk-go v1.6.2 h1:nxtaaU3hDD5x6gmoxs/qijSJqZrjFapYYuTiVCEgobA=
|
||||
github.com/ks3sdklib/aws-sdk-go v1.6.2/go.mod h1:jGcsV0dJgMmStAyqjkKVUu6F167pAXYZAS3LqoZMmtM=
|
||||
github.com/kylelemons/go-gypsy v1.0.0/go.mod h1:chkXM0zjdpXOiqkCW1XcCHDfjfk14PH2KKkQWxfJUcU=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
@@ -660,8 +659,6 @@ 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=
|
||||
@@ -968,6 +965,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=
|
||||
@@ -1052,6 +1051,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=
|
||||
@@ -1096,6 +1099,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=
|
||||
@@ -1152,6 +1159,11 @@ 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.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=
|
||||
@@ -1182,6 +1194,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=
|
||||
@@ -1258,12 +1275,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=
|
||||
@@ -1274,6 +1303,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=
|
||||
@@ -1349,6 +1384,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=
|
||||
@@ -1485,8 +1523,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=
|
||||
@@ -1502,8 +1538,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=
|
||||
|
||||
@@ -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
|
||||
@@ -211,6 +217,8 @@ type FileClient interface {
|
||||
ListEntities(ctx context.Context, args *ListEntityParameters) (*ListEntityResult, error)
|
||||
// UpdateProps updates props of a file
|
||||
UpdateProps(ctx context.Context, file *ent.File, props *types.FileProps) (*ent.File, error)
|
||||
// UpdateModifiedAt updates modified at of a file
|
||||
UpdateModifiedAt(ctx context.Context, file *ent.File, modifiedAt time.Time) error
|
||||
}
|
||||
|
||||
func NewFileClient(client *ent.Client, dbType conf.DBType, hasher hashid.Encoder) FileClient {
|
||||
@@ -640,6 +648,10 @@ func (f *fileClient) Copy(ctx context.Context, files []*ent.File, dstMap map[int
|
||||
return newDstMap, map[int]int64{dstMap[files[0].FileChildren][0].OwnerID: sizeDiff}, nil
|
||||
}
|
||||
|
||||
func (f *fileClient) UpdateModifiedAt(ctx context.Context, file *ent.File, modifiedAt time.Time) error {
|
||||
return f.client.File.UpdateOne(file).SetUpdatedAt(modifiedAt).Exec(ctx)
|
||||
}
|
||||
|
||||
func (f *fileClient) UpsertMetadata(ctx context.Context, file *ent.File, data map[string]string, privateMask map[string]bool) error {
|
||||
// Validate value length
|
||||
for key, value := range data {
|
||||
@@ -712,10 +724,15 @@ func (f *fileClient) UpgradePlaceholder(ctx context.Context, file *ent.File, mod
|
||||
}
|
||||
|
||||
if entityType == types.EntityTypeVersion {
|
||||
if err := f.client.File.UpdateOne(file).
|
||||
stm := f.client.File.UpdateOne(file).
|
||||
SetSize(placeholder.Size).
|
||||
SetPrimaryEntity(placeholder.ID).
|
||||
Exec(ctx); err != nil {
|
||||
SetPrimaryEntity(placeholder.ID)
|
||||
|
||||
if modifiedAt != nil {
|
||||
stm.SetUpdatedAt(*modifiedAt)
|
||||
}
|
||||
|
||||
if err := stm.Exec(ctx); err != nil {
|
||||
return fmt.Errorf("failed to upgrade file primary entity: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -890,7 +907,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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{
|
||||
@@ -414,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",
|
||||
@@ -501,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() {
|
||||
@@ -516,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"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||
)
|
||||
@@ -60,6 +61,22 @@ func WithTx[T TxOperator](ctx context.Context, c T) (T, *Tx, context.Context, er
|
||||
return c.SetClient(txClient).(T), txWrapper, ctx, nil
|
||||
}
|
||||
|
||||
// InheritTx wraps the given inventory client with a transaction.
|
||||
// If the transaction is already in the context, it will be inherited.
|
||||
// Otherwise, original client will be returned.
|
||||
func InheritTx[T TxOperator](ctx context.Context, c T) (T, *Tx) {
|
||||
var txClient *ent.Client
|
||||
var txWrapper *Tx
|
||||
|
||||
if txInherited, ok := ctx.Value(TxCtx{}).(*Tx); ok && !txInherited.finished {
|
||||
txWrapper = &Tx{inherited: true, tx: txInherited.tx, parent: txInherited}
|
||||
txClient = txWrapper.tx.Client()
|
||||
return c.SetClient(txClient).(T), txWrapper
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func Rollback(tx *Tx) error {
|
||||
if !tx.inherited {
|
||||
tx.finished = true
|
||||
|
||||
@@ -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"`
|
||||
@@ -41,6 +44,12 @@ type (
|
||||
Token string `json:"token"`
|
||||
// 允许的文件扩展名
|
||||
FileType []string `json:"file_type"`
|
||||
// IsFileTypeDenyList Whether above list is a deny list.
|
||||
IsFileTypeDenyList bool `json:"is_file_type_deny_list,omitempty"`
|
||||
// FileRegexp 文件扩展名正则表达式
|
||||
NameRegexp string `json:"file_regexp,omitempty"`
|
||||
// IsNameRegexp Whether above regexp is a deny list.
|
||||
IsNameRegexpDenyList bool `json:"is_name_regexp_deny_list,omitempty"`
|
||||
// OauthRedirect Oauth 重定向地址
|
||||
OauthRedirect string `json:"od_redirect,omitempty"`
|
||||
// CustomProxy whether to use custom-proxy to get file content
|
||||
@@ -173,7 +182,8 @@ 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 {
|
||||
@@ -254,6 +264,7 @@ const (
|
||||
PolicyTypeOss = "oss"
|
||||
PolicyTypeCos = "cos"
|
||||
PolicyTypeS3 = "s3"
|
||||
PolicyTypeKs3 = "ks3"
|
||||
PolicyTypeOd = "onedrive"
|
||||
PolicyTypeRemote = "remote"
|
||||
PolicyTypeObs = "obs"
|
||||
@@ -278,26 +289,57 @@ 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"`
|
||||
Platform string `json:"platform,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"
|
||||
)
|
||||
|
||||
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(),
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
@@ -32,18 +32,18 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
supportDownloadOptions = map[string]bool{
|
||||
"cookie": true,
|
||||
"skip_checking": true,
|
||||
"root_folder": true,
|
||||
"rename": true,
|
||||
"upLimit": true,
|
||||
"dlLimit": true,
|
||||
"ratioLimit": true,
|
||||
"seedingTimeLimit": true,
|
||||
"autoTMM": true,
|
||||
"sequentialDownload": true,
|
||||
"firstLastPiecePrio": true,
|
||||
downloadOptionFormatTypes = map[string]string{
|
||||
"cookie": "%s",
|
||||
"skip_checking": "%s",
|
||||
"root_folder": "%s",
|
||||
"rename": "%s",
|
||||
"upLimit": "%.0f",
|
||||
"dlLimit": "%.0f",
|
||||
"ratioLimit": "%f",
|
||||
"seedingTimeLimit": "%.0f",
|
||||
"autoTMM": "%t",
|
||||
"sequentialDownload": "%s",
|
||||
"firstLastPiecePrio": "%t",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -271,15 +271,15 @@ func (c *qbittorrentClient) CreateTask(ctx context.Context, url string, options
|
||||
|
||||
// Apply global options
|
||||
for k, v := range c.options.Options {
|
||||
if _, ok := supportDownloadOptions[k]; ok {
|
||||
_ = formWriter.WriteField(k, fmt.Sprintf("%s", v))
|
||||
if _, ok := downloadOptionFormatTypes[k]; ok {
|
||||
_ = formWriter.WriteField(k, fmt.Sprintf(downloadOptionFormatTypes[k], v))
|
||||
}
|
||||
}
|
||||
|
||||
// Apply group options
|
||||
for k, v := range options {
|
||||
if _, ok := supportDownloadOptions[k]; ok {
|
||||
_ = formWriter.WriteField(k, fmt.Sprintf("%s", v))
|
||||
if _, ok := downloadOptionFormatTypes[k]; ok {
|
||||
_ = formWriter.WriteField(k, fmt.Sprintf(downloadOptionFormatTypes[k], v))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,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 +37,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 +82,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 +121,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
|
||||
tlsPolicy := mail.TLSOpportunistic
|
||||
if client.config.ForceEncryption {
|
||||
d.SSL = true
|
||||
tlsPolicy = mail.TLSMandatory
|
||||
}
|
||||
d.StartTLSPolicy = mail.OpportunisticStartTLS
|
||||
|
||||
var s mail.SendCloser
|
||||
d, diaErr := mail.NewClient(client.config.Host,
|
||||
mail.WithPort(client.config.Port),
|
||||
mail.WithTimeout(time.Duration(client.config.Keepalive+5)*time.Second),
|
||||
mail.WithSMTPAuth(mail.SMTPAuthAutoDiscover), mail.WithTLSPortPolicy(tlsPolicy),
|
||||
mail.WithUsername(client.config.User), mail.WithPassword(client.config.Password),
|
||||
)
|
||||
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 +151,22 @@ 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 {
|
||||
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
|
||||
|
||||
551
pkg/filemanager/driver/ks3/ks3.go
Normal file
551
pkg/filemanager/driver/ks3/ks3.go
Normal file
@@ -0,0 +1,551 @@
|
||||
package ks3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"strconv"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/boolset"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/conf"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/chunk"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/chunk/backoff"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/mime"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
"github.com/ks3sdklib/aws-sdk-go/aws/awserr"
|
||||
"github.com/ks3sdklib/aws-sdk-go/service/s3/s3manager"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/ks3sdklib/aws-sdk-go/aws"
|
||||
"github.com/ks3sdklib/aws-sdk-go/aws/credentials"
|
||||
|
||||
"github.com/ks3sdklib/aws-sdk-go/service/s3"
|
||||
)
|
||||
|
||||
// Driver KS3 compatible driver
|
||||
type Driver struct {
|
||||
policy *ent.StoragePolicy
|
||||
chunkSize int64
|
||||
|
||||
settings setting.Provider
|
||||
l logging.Logger
|
||||
config conf.ConfigProvider
|
||||
mime mime.MimeDetector
|
||||
|
||||
sess *aws.Config
|
||||
svc *s3.S3
|
||||
}
|
||||
|
||||
// UploadPolicy KS3上传策略
|
||||
type UploadPolicy struct {
|
||||
Expiration string `json:"expiration"`
|
||||
Conditions []interface{} `json:"conditions"`
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
Config *aws.Config
|
||||
Handlers request.Handlers
|
||||
}
|
||||
|
||||
// MetaData 文件信息
|
||||
type MetaData struct {
|
||||
Size int64
|
||||
Etag string
|
||||
}
|
||||
|
||||
var (
|
||||
features = &boolset.BooleanSet{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
boolset.Sets(map[driver.HandlerCapability]bool{
|
||||
driver.HandlerCapabilityUploadSentinelRequired: true,
|
||||
}, features)
|
||||
}
|
||||
|
||||
func Int64(v int64) *int64 {
|
||||
return &v
|
||||
}
|
||||
|
||||
func New(ctx context.Context, policy *ent.StoragePolicy, settings setting.Provider,
|
||||
config conf.ConfigProvider, l logging.Logger, mime mime.MimeDetector) (*Driver, error) {
|
||||
chunkSize := policy.Settings.ChunkSize
|
||||
if policy.Settings.ChunkSize == 0 {
|
||||
chunkSize = 25 << 20 // 25 MB
|
||||
}
|
||||
|
||||
driver := &Driver{
|
||||
policy: policy,
|
||||
settings: settings,
|
||||
chunkSize: chunkSize,
|
||||
config: config,
|
||||
l: l,
|
||||
mime: mime,
|
||||
}
|
||||
|
||||
sess := aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials(policy.AccessKey, policy.SecretKey, ""),
|
||||
Endpoint: policy.Server,
|
||||
Region: policy.Settings.Region,
|
||||
S3ForcePathStyle: policy.Settings.S3ForcePathStyle,
|
||||
}
|
||||
driver.sess = &sess
|
||||
driver.svc = s3.New(&sess)
|
||||
|
||||
return driver, nil
|
||||
}
|
||||
|
||||
// List 列出给定路径下的文件
|
||||
func (handler *Driver) List(ctx context.Context, base string, onProgress driver.ListProgressFunc, recursive bool) ([]fs.PhysicalObject, error) {
|
||||
// 初始化列目录参数
|
||||
base = strings.TrimPrefix(base, "/")
|
||||
if base != "" {
|
||||
base += "/"
|
||||
}
|
||||
|
||||
opt := &s3.ListObjectsInput{
|
||||
Bucket: &handler.policy.BucketName,
|
||||
Prefix: &base,
|
||||
MaxKeys: Int64(1000),
|
||||
}
|
||||
|
||||
// 是否为递归列出
|
||||
if !recursive {
|
||||
opt.Delimiter = aws.String("/")
|
||||
}
|
||||
|
||||
var (
|
||||
objects []*s3.Object
|
||||
commons []*s3.CommonPrefix
|
||||
)
|
||||
|
||||
for {
|
||||
res, err := handler.svc.ListObjectsWithContext(ctx, opt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
objects = append(objects, res.Contents...)
|
||||
commons = append(commons, res.CommonPrefixes...)
|
||||
|
||||
// 如果本次未列取完,则继续使用marker获取结果
|
||||
if *res.IsTruncated {
|
||||
opt.Marker = res.NextMarker
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 处理列取结果
|
||||
res := make([]fs.PhysicalObject, 0, len(objects)+len(commons))
|
||||
|
||||
// 处理目录
|
||||
for _, object := range commons {
|
||||
rel, err := filepath.Rel(*opt.Prefix, *object.Prefix)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
res = append(res, fs.PhysicalObject{
|
||||
Name: path.Base(*object.Prefix),
|
||||
RelativePath: filepath.ToSlash(rel),
|
||||
Size: 0,
|
||||
IsDir: true,
|
||||
LastModify: time.Now(),
|
||||
})
|
||||
}
|
||||
onProgress(len(commons))
|
||||
|
||||
// 处理文件
|
||||
for _, object := range objects {
|
||||
rel, err := filepath.Rel(*opt.Prefix, *object.Key)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
res = append(res, fs.PhysicalObject{
|
||||
Name: path.Base(*object.Key),
|
||||
Source: *object.Key,
|
||||
RelativePath: filepath.ToSlash(rel),
|
||||
Size: *object.Size,
|
||||
IsDir: false,
|
||||
LastModify: time.Now(),
|
||||
})
|
||||
}
|
||||
onProgress(len(objects))
|
||||
|
||||
return res, nil
|
||||
|
||||
}
|
||||
|
||||
// Open 打开文件
|
||||
func (handler *Driver) Open(ctx context.Context, path string) (*os.File, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// Put 将文件流保存到指定目录
|
||||
func (handler *Driver) Put(ctx context.Context, file *fs.UploadRequest) error {
|
||||
defer file.Close()
|
||||
|
||||
// 是否允许覆盖
|
||||
overwrite := file.Mode&fs.ModeOverwrite == fs.ModeOverwrite
|
||||
if !overwrite {
|
||||
// Check for duplicated file
|
||||
if _, err := handler.Meta(ctx, file.Props.SavePath); err == nil {
|
||||
return fs.ErrFileExisted
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化配置
|
||||
uploader := s3manager.NewUploader(&s3manager.UploadOptions{
|
||||
S3: handler.svc, // S3Client实例,必填
|
||||
PartSize: handler.chunkSize, // 分块大小,默认为5MB,非必填
|
||||
})
|
||||
|
||||
mimeType := file.Props.MimeType
|
||||
if mimeType == "" {
|
||||
handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
}
|
||||
|
||||
_, err := uploader.UploadWithContext(ctx, &s3manager.UploadInput{
|
||||
Bucket: &handler.policy.BucketName,
|
||||
Key: &file.Props.SavePath,
|
||||
Body: io.LimitReader(file, file.Props.Size),
|
||||
ContentType: aws.String(mimeType),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除文件
|
||||
func (handler *Driver) Delete(ctx context.Context, files ...string) ([]string, error) {
|
||||
failed := make([]string, 0, len(files))
|
||||
batchSize := handler.policy.Settings.S3DeleteBatchSize
|
||||
if batchSize == 0 {
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html
|
||||
// The request can contain a list of up to 1000 keys that you want to delete.
|
||||
batchSize = 1000
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
|
||||
groups := lo.Chunk(files, batchSize)
|
||||
for _, group := range groups {
|
||||
if len(group) == 1 {
|
||||
// Invoke single file delete API
|
||||
_, err := handler.svc.DeleteObjectWithContext(ctx, &s3.DeleteObjectInput{
|
||||
Bucket: &handler.policy.BucketName,
|
||||
Key: &group[0],
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if aerr, ok := err.(awserr.Error); ok {
|
||||
// Ignore NoSuchKey error
|
||||
if aerr.Code() == s3.ErrCodeNoSuchKey {
|
||||
continue
|
||||
}
|
||||
}
|
||||
failed = append(failed, group[0])
|
||||
lastErr = err
|
||||
}
|
||||
} else {
|
||||
// Invoke batch delete API
|
||||
res, err := handler.svc.DeleteObjects(
|
||||
&s3.DeleteObjectsInput{
|
||||
Bucket: &handler.policy.BucketName,
|
||||
Delete: &s3.Delete{
|
||||
Objects: lo.Map(group, func(s string, i int) *s3.ObjectIdentifier {
|
||||
return &s3.ObjectIdentifier{Key: &s}
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
failed = append(failed, group...)
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
|
||||
for _, v := range res.Errors {
|
||||
handler.l.Debug("Failed to delete file: %s, Code:%s, Message:%s", v.Key, v.Code, v.Key)
|
||||
failed = append(failed, *v.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return failed, lastErr
|
||||
|
||||
}
|
||||
|
||||
// Thumb 获取缩略图URL
|
||||
func (handler *Driver) Thumb(ctx context.Context, expire *time.Time, ext string, e fs.Entity) (string, error) {
|
||||
return "", errors.New("not implemented")
|
||||
}
|
||||
|
||||
// Source 获取文件外链
|
||||
func (handler *Driver) Source(ctx context.Context, e fs.Entity, args *driver.GetSourceArgs) (string, error) {
|
||||
var contentDescription *string
|
||||
if args.IsDownload {
|
||||
encodedFilename := url.PathEscape(args.DisplayName)
|
||||
contentDescription = aws.String(fmt.Sprintf(`attachment; filename="%s"`, encodedFilename))
|
||||
}
|
||||
|
||||
// 确保过期时间不小于 0 ,如果小于则设置为 7 天
|
||||
var ttl int64
|
||||
if args.Expire != nil {
|
||||
ttl = int64(time.Until(*args.Expire).Seconds())
|
||||
} else {
|
||||
ttl = 604800
|
||||
}
|
||||
|
||||
downloadUrl, err := handler.svc.GeneratePresignedUrl(&s3.GeneratePresignedUrlInput{
|
||||
HTTPMethod: s3.GET, // 请求方法
|
||||
Bucket: &handler.policy.BucketName, // 存储空间名称
|
||||
Key: aws.String(e.Source()), // 对象的key
|
||||
Expires: ttl, // 过期时间,转换为秒数
|
||||
ResponseContentDisposition: contentDescription, // 设置响应头部 Content-Disposition
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 将最终生成的签名URL域名换成用户自定义的加速域名(如果有)
|
||||
finalURL, err := url.Parse(downloadUrl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 公有空间替换掉Key及不支持的头
|
||||
if !handler.policy.IsPrivate {
|
||||
finalURL.RawQuery = ""
|
||||
}
|
||||
|
||||
return finalURL.String(), nil
|
||||
}
|
||||
|
||||
// Token 获取上传凭证
|
||||
func (handler *Driver) Token(ctx context.Context, uploadSession *fs.UploadSession, file *fs.UploadRequest) (*fs.UploadCredential, error) {
|
||||
// Check for duplicated file
|
||||
if _, err := handler.Meta(ctx, file.Props.SavePath); err == nil {
|
||||
return nil, fs.ErrFileExisted
|
||||
}
|
||||
|
||||
// 生成回调地址
|
||||
siteURL := handler.settings.SiteURL(setting.UseFirstSiteUrl(ctx))
|
||||
// 在从机端创建上传会话
|
||||
uploadSession.ChunkSize = handler.chunkSize
|
||||
uploadSession.Callback = routes.MasterSlaveCallbackUrl(siteURL, types.PolicyTypeKs3, uploadSession.Props.UploadSessionID, uploadSession.CallbackSecret).String()
|
||||
|
||||
mimeType := file.Props.MimeType
|
||||
if mimeType == "" {
|
||||
handler.mime.TypeByName(file.Props.Uri.Name())
|
||||
}
|
||||
|
||||
// 创建分片上传
|
||||
res, err := handler.svc.CreateMultipartUploadWithContext(ctx, &s3.CreateMultipartUploadInput{
|
||||
Bucket: &handler.policy.BucketName,
|
||||
Key: &uploadSession.Props.SavePath,
|
||||
Expires: &uploadSession.Props.ExpireAt,
|
||||
ContentType: aws.String(mimeType),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create multipart upload: %w", err)
|
||||
}
|
||||
|
||||
uploadSession.UploadID = *res.UploadID
|
||||
|
||||
// 为每个分片签名上传 URL
|
||||
chunks := chunk.NewChunkGroup(file, handler.chunkSize, &backoff.ConstantBackoff{}, false, handler.l, "")
|
||||
urls := make([]string, chunks.Num())
|
||||
for chunks.Next() {
|
||||
err := chunks.Process(func(c *chunk.ChunkGroup, chunk io.Reader) error {
|
||||
// 计算过期时间(秒)
|
||||
expireSeconds := int(time.Until(uploadSession.Props.ExpireAt).Seconds())
|
||||
partNumber := c.Index() + 1
|
||||
|
||||
// 生成预签名URL
|
||||
signedURL, err := handler.svc.GeneratePresignedUrl(&s3.GeneratePresignedUrlInput{
|
||||
HTTPMethod: s3.PUT,
|
||||
Bucket: &handler.policy.BucketName,
|
||||
Key: &uploadSession.Props.SavePath,
|
||||
Expires: int64(expireSeconds),
|
||||
Parameters: map[string]*string{
|
||||
"partNumber": aws.String(strconv.Itoa(partNumber)),
|
||||
"uploadId": res.UploadID,
|
||||
},
|
||||
ContentType: aws.String("application/octet-stream"),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate presigned upload url for chunk %d: %w", partNumber, err)
|
||||
}
|
||||
urls[c.Index()] = signedURL
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 签名完成分片上传的请求URL
|
||||
expireSeconds := int(time.Until(uploadSession.Props.ExpireAt).Seconds())
|
||||
signedURL, err := handler.svc.GeneratePresignedUrl(&s3.GeneratePresignedUrlInput{
|
||||
HTTPMethod: s3.POST,
|
||||
Bucket: &handler.policy.BucketName,
|
||||
Key: &file.Props.SavePath,
|
||||
Expires: int64(expireSeconds),
|
||||
Parameters: map[string]*string{
|
||||
"uploadId": res.UploadID,
|
||||
},
|
||||
ContentType: aws.String("application/octet-stream"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 生成上传凭证
|
||||
return &fs.UploadCredential{
|
||||
UploadID: *res.UploadID,
|
||||
UploadURLs: urls,
|
||||
CompleteURL: signedURL,
|
||||
SessionID: uploadSession.Props.UploadSessionID,
|
||||
ChunkSize: handler.chunkSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CancelToken 取消上传凭证
|
||||
func (handler *Driver) CancelToken(ctx context.Context, uploadSession *fs.UploadSession) error {
|
||||
_, err := handler.svc.AbortMultipartUploadWithContext(ctx, &s3.AbortMultipartUploadInput{
|
||||
UploadID: &uploadSession.UploadID,
|
||||
Bucket: &handler.policy.BucketName,
|
||||
Key: &uploadSession.Props.SavePath,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// cancelUpload 取消分片上传
|
||||
func (handler *Driver) cancelUpload(key, id *string) {
|
||||
if _, err := handler.svc.AbortMultipartUpload(&s3.AbortMultipartUploadInput{
|
||||
Bucket: &handler.policy.BucketName,
|
||||
UploadID: id,
|
||||
Key: key,
|
||||
}); err != nil {
|
||||
handler.l.Warning("failed to abort multipart upload: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Capabilities 获取存储能力
|
||||
func (handler *Driver) Capabilities() *driver.Capabilities {
|
||||
return &driver.Capabilities{
|
||||
StaticFeatures: features,
|
||||
MediaMetaProxy: handler.policy.Settings.MediaMetaGeneratorProxy,
|
||||
ThumbProxy: handler.policy.Settings.ThumbGeneratorProxy,
|
||||
MaxSourceExpire: time.Duration(604800) * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// MediaMeta 获取媒体元信息
|
||||
func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// LocalPath 获取本地路径
|
||||
func (handler *Driver) LocalPath(ctx context.Context, path string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// CompleteUpload 完成上传
|
||||
func (handler *Driver) CompleteUpload(ctx context.Context, session *fs.UploadSession) error {
|
||||
if session.SentinelTaskID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make sure uploaded file size is correct
|
||||
res, err := handler.Meta(ctx, session.Props.SavePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get uploaded file size: %w", err)
|
||||
}
|
||||
|
||||
if res.Size != session.Props.Size {
|
||||
return serializer.NewError(
|
||||
serializer.CodeMetaMismatch,
|
||||
fmt.Sprintf("File size not match, expected: %d, actual: %d", session.Props.Size, res.Size),
|
||||
nil,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Meta 获取文件元信息
|
||||
func (handler *Driver) Meta(ctx context.Context, path string) (*MetaData, error) {
|
||||
res, err := handler.svc.HeadObjectWithContext(ctx,
|
||||
&s3.HeadObjectInput{
|
||||
Bucket: &handler.policy.BucketName,
|
||||
Key: &path,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MetaData{
|
||||
Size: *res.ContentLength,
|
||||
Etag: *res.ETag,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
// CORS 设置CORS规则
|
||||
func (handler *Driver) CORS() error {
|
||||
rule := s3.CORSRule{
|
||||
AllowedMethod: []string{
|
||||
"GET",
|
||||
"POST",
|
||||
"PUT",
|
||||
"DELETE",
|
||||
"HEAD",
|
||||
},
|
||||
AllowedOrigin: []string{"*"},
|
||||
AllowedHeader: []string{"*"},
|
||||
ExposeHeader: []string{"ETag"},
|
||||
MaxAgeSeconds: 3600,
|
||||
}
|
||||
|
||||
_, err := handler.svc.PutBucketCORS(&s3.PutBucketCORSInput{
|
||||
Bucket: &handler.policy.BucketName,
|
||||
CORSConfiguration: &s3.CORSConfiguration{
|
||||
Rules: []*s3.CORSRule{&rule},
|
||||
},
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Reader 读取器
|
||||
type Reader struct {
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
// Read 读取数据
|
||||
func (r Reader) Read(p []byte) (int, error) {
|
||||
return r.r.Read(p)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"math/rand"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -651,7 +652,8 @@ func (f *DBFS) getPreferredPolicy(ctx context.Context, file *File) (*ent.Storage
|
||||
return nil, fmt.Errorf("owner group not loaded")
|
||||
}
|
||||
|
||||
groupPolicy, err := f.storagePolicyClient.GetByGroup(ctx, ownerGroup)
|
||||
sc, _ := inventory.InheritTx(ctx, f.storagePolicyClient)
|
||||
groupPolicy, err := sc.GetByGroup(ctx, ownerGroup)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get available storage policies", err)
|
||||
}
|
||||
@@ -765,44 +767,72 @@ func (f *DBFS) navigatorId(path *fs.URI) string {
|
||||
|
||||
// generateSavePath generates the physical save path for the upload request.
|
||||
func generateSavePath(policy *ent.StoragePolicy, req *fs.UploadRequest, user *ent.User) string {
|
||||
baseTable := map[string]string{
|
||||
"{randomkey16}": util.RandStringRunes(16),
|
||||
"{randomkey8}": util.RandStringRunes(8),
|
||||
"{timestamp}": strconv.FormatInt(time.Now().Unix(), 10),
|
||||
"{timestamp_nano}": strconv.FormatInt(time.Now().UnixNano(), 10),
|
||||
"{randomnum2}": strconv.Itoa(rand.Intn(2)),
|
||||
"{randomnum3}": strconv.Itoa(rand.Intn(3)),
|
||||
"{randomnum4}": strconv.Itoa(rand.Intn(4)),
|
||||
"{randomnum8}": strconv.Itoa(rand.Intn(8)),
|
||||
"{uid}": strconv.Itoa(user.ID),
|
||||
"{datetime}": time.Now().Format("20060102150405"),
|
||||
"{date}": time.Now().Format("20060102"),
|
||||
"{year}": time.Now().Format("2006"),
|
||||
"{month}": time.Now().Format("01"),
|
||||
"{day}": time.Now().Format("02"),
|
||||
"{hour}": time.Now().Format("15"),
|
||||
"{minute}": time.Now().Format("04"),
|
||||
"{second}": time.Now().Format("05"),
|
||||
currentTime := time.Now()
|
||||
originName := req.Props.Uri.Name()
|
||||
|
||||
dynamicReplace := func(regPattern string, rule string, pathAvailable bool) string {
|
||||
re := regexp.MustCompile(regPattern)
|
||||
return re.ReplaceAllStringFunc(rule, func(match string) string {
|
||||
switch match {
|
||||
case "{timestamp}":
|
||||
return strconv.FormatInt(currentTime.Unix(), 10)
|
||||
case "{timestamp_nano}":
|
||||
return strconv.FormatInt(currentTime.UnixNano(), 10)
|
||||
case "{datetime}":
|
||||
return currentTime.Format("20060102150405")
|
||||
case "{date}":
|
||||
return currentTime.Format("20060102")
|
||||
case "{year}":
|
||||
return currentTime.Format("2006")
|
||||
case "{month}":
|
||||
return currentTime.Format("01")
|
||||
case "{day}":
|
||||
return currentTime.Format("02")
|
||||
case "{hour}":
|
||||
return currentTime.Format("15")
|
||||
case "{minute}":
|
||||
return currentTime.Format("04")
|
||||
case "{second}":
|
||||
return currentTime.Format("05")
|
||||
case "{uid}":
|
||||
return strconv.Itoa(user.ID)
|
||||
case "{randomkey16}":
|
||||
return util.RandStringRunes(16)
|
||||
case "{randomkey8}":
|
||||
return util.RandStringRunes(8)
|
||||
case "{randomnum8}":
|
||||
return strconv.Itoa(rand.Intn(8))
|
||||
case "{randomnum4}":
|
||||
return strconv.Itoa(rand.Intn(4))
|
||||
case "{randomnum3}":
|
||||
return strconv.Itoa(rand.Intn(3))
|
||||
case "{randomnum2}":
|
||||
return strconv.Itoa(rand.Intn(2))
|
||||
case "{uuid}":
|
||||
return uuid.Must(uuid.NewV4()).String()
|
||||
case "{path}":
|
||||
if pathAvailable {
|
||||
return req.Props.Uri.Dir() + fs.Separator
|
||||
}
|
||||
return match
|
||||
case "{originname}":
|
||||
return originName
|
||||
case "{ext}":
|
||||
return filepath.Ext(originName)
|
||||
case "{originname_without_ext}":
|
||||
return strings.TrimSuffix(originName, filepath.Ext(originName))
|
||||
default:
|
||||
return match
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
dirRule := policy.DirNameRule
|
||||
dirRule = filepath.ToSlash(dirRule)
|
||||
dirRule = util.Replace(baseTable, dirRule)
|
||||
dirRule = util.Replace(map[string]string{
|
||||
"{path}": req.Props.Uri.Dir() + fs.Separator,
|
||||
}, dirRule)
|
||||
|
||||
originName := req.Props.Uri.Name()
|
||||
nameTable := map[string]string{
|
||||
"{originname}": originName,
|
||||
"{ext}": filepath.Ext(originName),
|
||||
"{originname_without_ext}": strings.TrimSuffix(originName, filepath.Ext(originName)),
|
||||
"{uuid}": uuid.Must(uuid.NewV4()).String(),
|
||||
}
|
||||
dirRule = dynamicReplace(`\{[^{}]+\}`, dirRule, true)
|
||||
|
||||
nameRule := policy.FileNameRule
|
||||
nameRule = util.Replace(baseTable, nameRule)
|
||||
nameRule = util.Replace(nameTable, nameRule)
|
||||
nameRule = dynamicReplace(`\{[^{}]+\}`, nameRule, false)
|
||||
|
||||
return path.Join(path.Clean(dirRule), nameRule)
|
||||
}
|
||||
|
||||
@@ -120,6 +120,20 @@ func (f *DBFS) Create(ctx context.Context, path *fs.URI, fileType types.FileType
|
||||
|
||||
ancestor = newFile(ancestor, newFolder)
|
||||
} else {
|
||||
// valide file name
|
||||
policy, err := f.getPreferredPolicy(ctx, ancestor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := validateExtension(desired[i], policy); err != nil {
|
||||
return nil, fs.ErrIllegalObjectName.WithError(err)
|
||||
}
|
||||
|
||||
if err := validateFileNameRegexp(desired[i], policy); err != nil {
|
||||
return nil, fs.ErrIllegalObjectName.WithError(err)
|
||||
}
|
||||
|
||||
file, err := f.createFile(ctx, ancestor, desired[i], fileType, o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -170,6 +184,10 @@ func (f *DBFS) Rename(ctx context.Context, path *fs.URI, newName string) (fs.Fil
|
||||
if err := validateExtension(newName, policy); err != nil {
|
||||
return nil, fs.ErrIllegalObjectName.WithError(err)
|
||||
}
|
||||
|
||||
if err := validateFileNameRegexp(newName, policy); err != nil {
|
||||
return nil, fs.ErrIllegalObjectName.WithError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Lock target
|
||||
|
||||
@@ -3,6 +3,7 @@ package dbfs
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
@@ -100,6 +101,7 @@ func (f *DBFS) PatchMetadata(ctx context.Context, path []*fs.URI, metas ...fs.Me
|
||||
metadataMap := make(map[string]string)
|
||||
privateMap := make(map[string]bool)
|
||||
deleted := make([]string, 0)
|
||||
updateModifiedAt := false
|
||||
for _, meta := range metas {
|
||||
if meta.Remove {
|
||||
deleted = append(deleted, meta.Key)
|
||||
@@ -109,6 +111,9 @@ func (f *DBFS) PatchMetadata(ctx context.Context, path []*fs.URI, metas ...fs.Me
|
||||
if meta.Private {
|
||||
privateMap[meta.Key] = meta.Private
|
||||
}
|
||||
if meta.UpdateModifiedAt {
|
||||
updateModifiedAt = true
|
||||
}
|
||||
}
|
||||
|
||||
fc, tx, ctx, err := inventory.WithTx(ctx, f.fileClient)
|
||||
@@ -128,6 +133,13 @@ func (f *DBFS) PatchMetadata(ctx context.Context, path []*fs.URI, metas ...fs.Me
|
||||
return fmt.Errorf("failed to remove metadata: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if updateModifiedAt {
|
||||
if err := fc.UpdateModifiedAt(ctx, target.Model, time.Now()); err != nil {
|
||||
_ = inventory.Rollback(tx)
|
||||
return fmt.Errorf("failed to update file modified at: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := inventory.Commit(tx); err != nil {
|
||||
|
||||
@@ -3,10 +3,12 @@ package dbfs
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const MaxFileNameLength = 256
|
||||
@@ -30,18 +32,35 @@ func validateFileName(name string) error {
|
||||
|
||||
// validateExtension validates the file extension.
|
||||
func validateExtension(name string, policy *ent.StoragePolicy) error {
|
||||
// 不需要验证
|
||||
if len(policy.Settings.FileType) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !util.IsInExtensionList(policy.Settings.FileType, name) {
|
||||
inList := util.IsInExtensionList(policy.Settings.FileType, name)
|
||||
if (policy.Settings.IsFileTypeDenyList && inList) || (!policy.Settings.IsFileTypeDenyList && !inList) {
|
||||
return fmt.Errorf("file extension is not allowed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateFileNameRegexp(name string, policy *ent.StoragePolicy) error {
|
||||
if policy.Settings.NameRegexp == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
match, err := regexp.MatchString(policy.Settings.NameRegexp, name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid file name regexp: %s", err)
|
||||
}
|
||||
|
||||
if (policy.Settings.IsNameRegexpDenyList && match) || (!policy.Settings.IsNameRegexpDenyList && !match) {
|
||||
return fmt.Errorf("file name is not allowed by regexp")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateFileSize validates the file size.
|
||||
func validateFileSize(size int64, policy *ent.StoragePolicy) error {
|
||||
if policy.MaxSize == 0 {
|
||||
@@ -56,11 +75,15 @@ func validateFileSize(size int64, policy *ent.StoragePolicy) error {
|
||||
// validateNewFile validates the upload request.
|
||||
func validateNewFile(fileName string, size int64, policy *ent.StoragePolicy) error {
|
||||
if err := validateFileName(fileName); err != nil {
|
||||
return err
|
||||
return fs.ErrIllegalObjectName.WithError(err)
|
||||
}
|
||||
|
||||
if err := validateExtension(fileName, policy); err != nil {
|
||||
return err
|
||||
return fs.ErrIllegalObjectName.WithError(err)
|
||||
}
|
||||
|
||||
if err := validateFileNameRegexp(fileName, policy); err != nil {
|
||||
return fs.ErrIllegalObjectName.WithError(err)
|
||||
}
|
||||
|
||||
if err := validateFileSize(size, policy); err != nil {
|
||||
|
||||
@@ -203,10 +203,11 @@ type (
|
||||
}
|
||||
|
||||
MetadataPatch struct {
|
||||
Key string `json:"key" binding:"required"`
|
||||
Value string `json:"value"`
|
||||
Private bool `json:"private" binding:"ne=true"`
|
||||
Remove bool `json:"remove"`
|
||||
Key string `json:"key" binding:"required"`
|
||||
Value string `json:"value"`
|
||||
Private bool `json:"private" binding:"ne=true"`
|
||||
Remove bool `json:"remove"`
|
||||
UpdateModifiedAt bool `json:"-"`
|
||||
}
|
||||
|
||||
// ListFileResult result of listing files.
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/cos"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/ks3"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/local"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/obs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/onedrive"
|
||||
@@ -73,6 +74,8 @@ func (m *manager) GetStorageDriver(ctx context.Context, policy *ent.StoragePolic
|
||||
return cos.New(ctx, policy, m.settings, m.config, m.l, m.dep.MimeDetector(ctx))
|
||||
case types.PolicyTypeS3:
|
||||
return s3.New(ctx, policy, m.settings, m.config, m.l, m.dep.MimeDetector(ctx))
|
||||
case types.PolicyTypeKs3:
|
||||
return ks3.New(ctx, policy, m.settings, m.config, m.l, m.dep.MimeDetector(ctx))
|
||||
case types.PolicyTypeObs:
|
||||
return obs.New(ctx, policy, m.settings, m.config, m.l, m.dep.MimeDetector(ctx))
|
||||
case types.PolicyTypeQiniu:
|
||||
|
||||
@@ -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 (
|
||||
@@ -38,6 +43,8 @@ var (
|
||||
// validateColor validates a color value
|
||||
validateColor = func(optional bool) metadataValidator {
|
||||
return func(ctx context.Context, m *manager, patch *fs.MetadataPatch) error {
|
||||
patch.UpdateModifiedAt = true
|
||||
|
||||
if patch.Remove {
|
||||
return nil
|
||||
}
|
||||
@@ -62,6 +69,8 @@ var (
|
||||
return fmt.Errorf("cannot remove system metadata")
|
||||
}
|
||||
|
||||
patch.UpdateModifiedAt = true
|
||||
|
||||
dep := dependency.FromContext(ctx)
|
||||
// Validate share owner is valid hashid
|
||||
if patch.Key == shareOwnerMetadataKey {
|
||||
@@ -91,6 +100,8 @@ var (
|
||||
customizeMetadataSuffix: {
|
||||
iconColorMetadataKey: validateColor(false),
|
||||
emojiIconMetadataKey: func(ctx context.Context, m *manager, patch *fs.MetadataPatch) error {
|
||||
patch.UpdateModifiedAt = true
|
||||
|
||||
if patch.Remove {
|
||||
return nil
|
||||
}
|
||||
@@ -120,6 +131,8 @@ var (
|
||||
},
|
||||
tagMetadataSuffix: {
|
||||
wildcardMetadataKey: func(ctx context.Context, m *manager, patch *fs.MetadataPatch) error {
|
||||
patch.UpdateModifiedAt = true
|
||||
|
||||
if err := validateColor(true)(ctx, m, patch); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -131,44 +144,170 @@ var (
|
||||
return nil
|
||||
},
|
||||
},
|
||||
customPropsMetadataSuffix: {
|
||||
wildcardMetadataKey: func(ctx context.Context, m *manager, patch *fs.MetadataPatch) error {
|
||||
patch.UpdateModifiedAt = true
|
||||
|
||||
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")
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func (m *manager) PatchMedata(ctx context.Context, path []*fs.URI, data ...fs.MetadataPatch) error {
|
||||
if err := m.validateMetadata(ctx, data...); err != nil {
|
||||
data, err := m.validateMetadata(ctx, data...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.fs.PatchMetadata(ctx, path, data...)
|
||||
}
|
||||
|
||||
func (m *manager) validateMetadata(ctx context.Context, data ...fs.MetadataPatch) error {
|
||||
func (m *manager) validateMetadata(ctx context.Context, data ...fs.MetadataPatch) ([]fs.MetadataPatch, error) {
|
||||
validated := make([]fs.MetadataPatch, 0, len(data))
|
||||
for _, patch := range data {
|
||||
category := strings.Split(patch.Key, ":")
|
||||
if len(category) < 2 {
|
||||
return serializer.NewError(serializer.CodeParamErr, "Invalid metadata key", nil)
|
||||
return validated, serializer.NewError(serializer.CodeParamErr, "Invalid metadata key", nil)
|
||||
}
|
||||
|
||||
categoryValidators, ok := validators[category[0]]
|
||||
if !ok {
|
||||
return serializer.NewError(serializer.CodeParamErr, "Invalid metadata key",
|
||||
return validated, serializer.NewError(serializer.CodeParamErr, "Invalid metadata key",
|
||||
fmt.Errorf("unknown category: %s", category[0]))
|
||||
}
|
||||
|
||||
// Explicit validators
|
||||
if v, ok := categoryValidators[patch.Key]; ok {
|
||||
if err := v(ctx, m, &patch); err != nil {
|
||||
return serializer.NewError(serializer.CodeParamErr, "Invalid metadata patch", err)
|
||||
return validated, serializer.NewError(serializer.CodeParamErr, "Invalid metadata patch", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Wildcard validators
|
||||
if v, ok := categoryValidators[wildcardMetadataKey]; ok {
|
||||
if err := v(ctx, m, &patch); err != nil {
|
||||
return serializer.NewError(serializer.CodeParamErr, "Invalid metadata patch", err)
|
||||
return validated, serializer.NewError(serializer.CodeParamErr, "Invalid metadata patch", err)
|
||||
}
|
||||
}
|
||||
|
||||
validated = append(validated, patch)
|
||||
}
|
||||
|
||||
return nil
|
||||
return validated, nil
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ func (m *manager) Create(ctx context.Context, path *fs.URI, fileType types.FileT
|
||||
|
||||
isSymbolic := false
|
||||
if o.Metadata != nil {
|
||||
if err := m.validateMetadata(ctx, lo.MapToSlice(o.Metadata, func(key string, value string) fs.MetadataPatch {
|
||||
_, err := m.validateMetadata(ctx, lo.MapToSlice(o.Metadata, func(key string, value string) fs.MetadataPatch {
|
||||
if key == shareRedirectMetadataKey {
|
||||
isSymbolic = true
|
||||
}
|
||||
@@ -124,7 +124,8 @@ func (m *manager) Create(ctx context.Context, path *fs.URI, fileType types.FileT
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
})...); err != nil {
|
||||
})...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,6 +311,7 @@ func CronCollectTrashBin(ctx context.Context) {
|
||||
res, err := fm.fs.AllFilesInTrashBin(ctx, fs.WithPageSize(pageSize))
|
||||
if err != nil {
|
||||
l.Error("Failed to get files in trash bin: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
expired := lo.Filter(res.Files, func(file fs.File, index int) bool {
|
||||
|
||||
@@ -55,7 +55,7 @@ func (m *manager) CreateUploadSession(ctx context.Context, req *fs.UploadRequest
|
||||
|
||||
// Validate metadata
|
||||
if req.Props.Metadata != nil {
|
||||
if err := m.validateMetadata(ctx, lo.MapToSlice(req.Props.Metadata, func(key string, value string) fs.MetadataPatch {
|
||||
if _, err := m.validateMetadata(ctx, lo.MapToSlice(req.Props.Metadata, func(key string, value string) fs.MetadataPatch {
|
||||
return fs.MetadataPatch{
|
||||
Key: key,
|
||||
Value: value,
|
||||
|
||||
@@ -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,
|
||||
@@ -431,25 +432,29 @@ func (m *RemoteDownloadTask) masterTransfer(ctx context.Context, dep dependency.
|
||||
ae := serializer.NewAggregateError()
|
||||
|
||||
transferFunc := func(workerId int, file downloader.TaskFile) {
|
||||
defer func() {
|
||||
atomic.AddInt64(&m.progress[ProgressTypeUploadCount].Current, 1)
|
||||
worker <- workerId
|
||||
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()
|
||||
m.progress[progressKey] = &queue.Progress{Identifier: dst.String(), Total: file.Size}
|
||||
fileProgress := m.progress[progressKey]
|
||||
uploadProgress := m.progress[ProgressTypeUpload]
|
||||
uploadCountProgress := m.progress[ProgressTypeUploadCount]
|
||||
m.Unlock()
|
||||
|
||||
defer func() {
|
||||
atomic.AddInt64(&uploadCountProgress.Current, 1)
|
||||
worker <- workerId
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
fileStream, err := os.Open(src)
|
||||
if err != nil {
|
||||
m.l.Warning("Failed to open file %s: %s", src, err.Error())
|
||||
atomic.AddInt64(&m.progress[ProgressTypeUpload].Current, file.Size)
|
||||
atomic.AddInt64(&uploadProgress.Current, file.Size)
|
||||
atomic.AddInt64(&failed, 1)
|
||||
ae.Add(file.Name, fmt.Errorf("failed to open file: %w", err))
|
||||
return
|
||||
@@ -463,8 +468,8 @@ func (m *RemoteDownloadTask) masterTransfer(ctx context.Context, dep dependency.
|
||||
Size: file.Size,
|
||||
},
|
||||
ProgressFunc: func(current, diff int64, total int64) {
|
||||
atomic.AddInt64(&m.progress[progressKey].Current, diff)
|
||||
atomic.AddInt64(&m.progress[ProgressTypeUpload].Current, diff)
|
||||
atomic.AddInt64(&fileProgress.Current, diff)
|
||||
atomic.AddInt64(&uploadProgress.Current, diff)
|
||||
},
|
||||
File: fileStream,
|
||||
}
|
||||
@@ -473,7 +478,7 @@ func (m *RemoteDownloadTask) masterTransfer(ctx context.Context, dep dependency.
|
||||
if err != nil {
|
||||
m.l.Warning("Failed to upload file %s: %s", src, err.Error())
|
||||
atomic.AddInt64(&failed, 1)
|
||||
atomic.AddInt64(&m.progress[ProgressTypeUpload].Current, file.Size)
|
||||
atomic.AddInt64(&uploadProgress.Current, file.Size)
|
||||
ae.Add(file.Name, fmt.Errorf("failed to upload file: %w", err))
|
||||
return
|
||||
}
|
||||
@@ -488,8 +493,10 @@ func (m *RemoteDownloadTask) masterTransfer(ctx context.Context, dep dependency.
|
||||
// Check if file is already transferred
|
||||
if _, ok := m.state.Transferred[file.Index]; ok {
|
||||
m.l.Info("File %s already transferred, skipping...", file.Name)
|
||||
m.Lock()
|
||||
atomic.AddInt64(&m.progress[ProgressTypeUpload].Current, file.Size)
|
||||
atomic.AddInt64(&m.progress[ProgressTypeUploadCount].Current, 1)
|
||||
m.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -538,7 +545,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 == "",
|
||||
}
|
||||
@@ -623,17 +630,21 @@ func (m *RemoteDownloadTask) Progress(ctx context.Context) queue.Progresses {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
if m.state.NodeState.progress != nil {
|
||||
merged := make(queue.Progresses)
|
||||
for k, v := range m.progress {
|
||||
merged[k] = v
|
||||
}
|
||||
merged := make(queue.Progresses)
|
||||
for k, v := range m.progress {
|
||||
merged[k] = v
|
||||
}
|
||||
|
||||
if m.state.NodeState.progress != nil {
|
||||
for k, v := range m.state.NodeState.progress {
|
||||
merged[k] = v
|
||||
}
|
||||
|
||||
return merged
|
||||
}
|
||||
return m.progress
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
func sanitizeFileName(name string) string {
|
||||
r := strings.NewReplacer("\\", "_", ":", "_", "*", "_", "?", "_", "\"", "_", "<", "_", ">", "_", "|", "_")
|
||||
return r.Replace(name)
|
||||
}
|
||||
|
||||
@@ -115,23 +115,26 @@ func (t *SlaveUploadTask) Do(ctx context.Context) (task.Status, error) {
|
||||
atomic.StoreInt64(&t.progress[ProgressTypeUpload].Total, totalSize)
|
||||
ae := serializer.NewAggregateError()
|
||||
transferFunc := func(workerId, fileId int, file SlaveUploadEntity) {
|
||||
defer func() {
|
||||
atomic.AddInt64(&t.progress[ProgressTypeUploadCount].Current, 1)
|
||||
worker <- workerId
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
t.l.Info("Uploading file %s to %s...", file.Src, file.Uri.String())
|
||||
|
||||
progressKey := fmt.Sprintf("%s%d", ProgressTypeUploadSinglePrefix, workerId)
|
||||
t.Lock()
|
||||
t.progress[progressKey] = &queue.Progress{Identifier: file.Uri.String(), Total: file.Size}
|
||||
fileProgress := t.progress[progressKey]
|
||||
uploadProgress := t.progress[ProgressTypeUpload]
|
||||
uploadCountProgress := t.progress[ProgressTypeUploadCount]
|
||||
t.Unlock()
|
||||
|
||||
defer func() {
|
||||
atomic.AddInt64(&uploadCountProgress.Current, 1)
|
||||
worker <- workerId
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
handle, err := os.Open(filepath.FromSlash(file.Src))
|
||||
if err != nil {
|
||||
t.l.Warning("Failed to open file %s: %s", file.Src, err.Error())
|
||||
atomic.AddInt64(&t.progress[ProgressTypeUpload].Current, file.Size)
|
||||
atomic.AddInt64(&fileProgress.Current, file.Size)
|
||||
ae.Add(path.Base(file.Src), fmt.Errorf("failed to open file: %w", err))
|
||||
return
|
||||
}
|
||||
@@ -140,7 +143,7 @@ func (t *SlaveUploadTask) Do(ctx context.Context) (task.Status, error) {
|
||||
if err != nil {
|
||||
t.l.Warning("Failed to get file stat for %s: %s", file.Src, err.Error())
|
||||
handle.Close()
|
||||
atomic.AddInt64(&t.progress[ProgressTypeUpload].Current, file.Size)
|
||||
atomic.AddInt64(&fileProgress.Current, file.Size)
|
||||
ae.Add(path.Base(file.Src), fmt.Errorf("failed to get file stat: %w", err))
|
||||
return
|
||||
}
|
||||
@@ -151,9 +154,9 @@ func (t *SlaveUploadTask) Do(ctx context.Context) (task.Status, error) {
|
||||
Size: stat.Size(),
|
||||
},
|
||||
ProgressFunc: func(current, diff int64, total int64) {
|
||||
atomic.AddInt64(&t.progress[progressKey].Current, diff)
|
||||
atomic.AddInt64(&t.progress[ProgressTypeUpload].Current, diff)
|
||||
atomic.StoreInt64(&t.progress[progressKey].Total, total)
|
||||
atomic.AddInt64(&fileProgress.Current, diff)
|
||||
atomic.AddInt64(&uploadCountProgress.Current, 1)
|
||||
atomic.StoreInt64(&fileProgress.Total, total)
|
||||
},
|
||||
File: handle,
|
||||
Seeker: handle,
|
||||
@@ -163,7 +166,7 @@ func (t *SlaveUploadTask) Do(ctx context.Context) (task.Status, error) {
|
||||
if err != nil {
|
||||
handle.Close()
|
||||
t.l.Warning("Failed to upload file %s: %s", file.Src, err.Error())
|
||||
atomic.AddInt64(&t.progress[ProgressTypeUpload].Current, file.Size)
|
||||
atomic.AddInt64(&uploadProgress.Current, file.Size)
|
||||
ae.Add(path.Base(file.Src), fmt.Errorf("failed to upload file: %w", err))
|
||||
return
|
||||
}
|
||||
@@ -179,8 +182,10 @@ func (t *SlaveUploadTask) Do(ctx context.Context) (task.Status, error) {
|
||||
// Check if file is already transferred
|
||||
if _, ok := t.state.Transferred[fileId]; ok {
|
||||
t.l.Info("File %s already transferred, skipping...", file.Src)
|
||||
t.Lock()
|
||||
atomic.AddInt64(&t.progress[ProgressTypeUpload].Current, file.Size)
|
||||
atomic.AddInt64(&t.progress[ProgressTypeUploadCount].Current, 1)
|
||||
t.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -221,5 +226,9 @@ func (m *SlaveUploadTask) Progress(ctx context.Context) queue.Progresses {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
return m.progress
|
||||
res := make(queue.Progresses)
|
||||
for k, v := range m.progress {
|
||||
res[k] = v
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -209,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]"
|
||||
|
||||
@@ -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")
|
||||
@@ -489,6 +492,12 @@ func initMasterRouter(dep dependency.Dep) *gin.Engine {
|
||||
middleware.UseUploadSession(types.PolicyTypeS3),
|
||||
controllers.ProcessCallback(http.StatusBadRequest, false),
|
||||
)
|
||||
// 金山 ks3策略上传回调
|
||||
callback.GET(
|
||||
"ks3/:sessionID/:key",
|
||||
middleware.UseUploadSession(types.PolicyTypeKs3),
|
||||
controllers.ProcessCallback(http.StatusBadRequest, false),
|
||||
)
|
||||
// Huawei OBS upload callback
|
||||
callback.POST(
|
||||
"obs/:sessionID/:key",
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/credmanager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/cos"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/ks3"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/obs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/onedrive"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/oss"
|
||||
@@ -362,6 +363,17 @@ func (service *CreateStoragePolicyCorsService) Create(c *gin.Context) error {
|
||||
|
||||
return nil
|
||||
|
||||
case types.PolicyTypeKs3:
|
||||
handler, err := ks3.New(c, service.Policy, dep.SettingProvider(), dep.ConfigProvider(), dep.Logger(), dep.MimeDetector(c))
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to create ks3 driver", err)
|
||||
}
|
||||
|
||||
if err := handler.CORS(); err != nil {
|
||||
return serializer.NewError(serializer.CodeInternalSetting, "Failed to create cors: "+err.Error(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
case types.PolicyTypeObs:
|
||||
handler, err := obs.New(c, service.Policy, dep.SettingProvider(), dep.ConfigProvider(), dep.Logger(), dep.MimeDetector(c))
|
||||
if err != nil {
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"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 +14,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,26 +139,30 @@ 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
|
||||
tlsPolicy := mail.TLSOpportunistic
|
||||
if setting.IsTrueValue(s.Settings["smtpEncryption"]) {
|
||||
d.SSL = true
|
||||
tlsPolicy = mail.TLSMandatory
|
||||
}
|
||||
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)
|
||||
d, diaErr := mail.NewClient(s.Settings["smtpHost"],
|
||||
mail.WithPort(port),
|
||||
mail.WithSMTPAuth(mail.SMTPAuthAutoDiscover), mail.WithTLSPortPolicy(tlsPolicy),
|
||||
mail.WithUsername(s.Settings["smtpUser"]), mail.WithPassword(s.Settings["smtpPass"]),
|
||||
)
|
||||
if diaErr != nil {
|
||||
return serializer.NewError(serializer.CodeInternalSetting, "Failed to create SMTP client: "+diaErr.Error(), diaErr)
|
||||
}
|
||||
|
||||
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.")
|
||||
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 = mail.Send(sender, m)
|
||||
err = d.DialAndSendWithContext(c, m)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeInternalSetting, "Failed to send test email: "+err.Error(), err)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
@@ -45,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"`
|
||||
@@ -87,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 {
|
||||
@@ -102,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)
|
||||
@@ -125,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,
|
||||
@@ -141,6 +147,8 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) {
|
||||
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
|
||||
|
||||
@@ -250,12 +250,15 @@ type DirectLink struct {
|
||||
}
|
||||
|
||||
type StoragePolicy struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
AllowedSuffix []string `json:"allowed_suffix,omitempty"`
|
||||
Type types.PolicyType `json:"type"`
|
||||
MaxSize int64 `json:"max_size"`
|
||||
Relay bool `json:"relay,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
AllowedSuffix []string `json:"allowed_suffix,omitempty"`
|
||||
DeniedSuffix []string `json:"denied_suffix,omitempty"`
|
||||
AllowedNameRegexp string `json:"allowed_name_regexp,omitempty"`
|
||||
DeniedNameRegexp string `json:"denied_name_regexp,omitempty"`
|
||||
Type types.PolicyType `json:"type"`
|
||||
MaxSize int64 `json:"max_size"`
|
||||
Relay bool `json:"relay,omitempty"`
|
||||
}
|
||||
|
||||
type Entity struct {
|
||||
@@ -268,19 +271,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"`
|
||||
@@ -298,15 +302,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 {
|
||||
@@ -433,23 +438,42 @@ 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 {
|
||||
if sp == nil {
|
||||
return nil
|
||||
}
|
||||
return &StoragePolicy{
|
||||
ID: hashid.EncodePolicyID(hasher, sp.ID),
|
||||
Name: sp.Name,
|
||||
Type: types.PolicyType(sp.Type),
|
||||
MaxSize: sp.MaxSize,
|
||||
AllowedSuffix: sp.Settings.FileType,
|
||||
Relay: sp.Settings.Relay,
|
||||
|
||||
res := &StoragePolicy{
|
||||
ID: hashid.EncodePolicyID(hasher, sp.ID),
|
||||
Name: sp.Name,
|
||||
Type: types.PolicyType(sp.Type),
|
||||
MaxSize: sp.MaxSize,
|
||||
Relay: sp.Settings.Relay,
|
||||
}
|
||||
|
||||
if sp.Settings.IsFileTypeDenyList {
|
||||
res.DeniedSuffix = sp.Settings.FileType
|
||||
} else {
|
||||
res.AllowedSuffix = sp.Settings.FileType
|
||||
}
|
||||
|
||||
if sp.Settings.NameRegexp != "" {
|
||||
if sp.Settings.IsNameRegexpDenyList {
|
||||
res.DeniedNameRegexp = sp.Settings.NameRegexp
|
||||
} else {
|
||||
res.AllowedNameRegexp = sp.Settings.NameRegexp
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func WriteEventSourceHeader(c *gin.Context) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -137,6 +137,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 +156,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