From 9f9796f2f37c03523dc0f336c3f91f3e46587ff3 Mon Sep 17 00:00:00 2001 From: WittF Date: Thu, 19 Jun 2025 11:31:17 +0800 Subject: [PATCH 01/27] Add Cap Captcha support (#2511) * Add Cap Captcha support - Add CaptchaCap type constant in types.go - Add Cap struct with InstanceURL, KeyID, and KeySecret fields - Add CapCaptcha method in provider.go to return Cap settings - Add default settings for Cap captcha in setting.go - Implement Cap captcha verification logic in middleware - Expose Cap captcha settings in site API This adds support for Cap captcha service as an alternative captcha option alongside existing reCAPTCHA, Turnstile and built-in captcha options. * update cap json tags --- inventory/setting.go | 3 +++ middleware/captcha.go | 58 +++++++++++++++++++++++++++++++++++++++++ pkg/setting/provider.go | 10 +++++++ pkg/setting/types.go | 7 +++++ service/basic/site.go | 5 ++++ 5 files changed, 83 insertions(+) diff --git a/inventory/setting.go b/inventory/setting.go index 8d48439c..9bf9b2f3 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -143,6 +143,9 @@ var DefaultSettings = map[string]string{ "captcha_ReCaptchaSecret": "defaultSecret", "captcha_turnstile_site_key": "", "captcha_turnstile_site_secret": "", + "captcha_cap_instance_url": "", + "captcha_cap_key_id": "", + "captcha_cap_key_secret": "", "thumb_width": "400", "thumb_height": "300", "thumb_entity_suffix": "._thumb", diff --git a/middleware/captcha.go b/middleware/captcha.go index b97c9ba5..af72433a 100644 --- a/middleware/captcha.go +++ b/middleware/captcha.go @@ -38,6 +38,9 @@ type ( turnstileResponse struct { Success bool `json:"success"` } + capResponse struct { + Success bool `json:"success"` + } ) // CaptchaRequired 验证请求签名 @@ -127,6 +130,61 @@ func CaptchaRequired(enabled func(c *gin.Context) bool) gin.HandlerFunc { return } + break + case setting.CaptchaCap: + captchaSetting := settings.CapCaptcha(c) + if captchaSetting.InstanceURL == "" || captchaSetting.KeyID == "" || captchaSetting.KeySecret == "" { + l.Warning("Cap verification failed: missing configuration") + c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha configuration error", nil)) + c.Abort() + return + } + + r := dep.RequestClient( + request2.WithContext(c), + request2.WithLogger(logging.FromContext(c)), + request2.WithHeader(http.Header{"Content-Type": []string{"application/json"}}), + ) + + capEndpoint := strings.TrimSuffix(captchaSetting.InstanceURL, "/") + "/" + captchaSetting.KeyID + "/siteverify" + requestBody := map[string]string{ + "secret": captchaSetting.KeySecret, + "response": service.Ticket, + } + requestData, err := json.Marshal(requestBody) + if err != nil { + l.Warning("Cap verification failed: %s", err) + c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", err)) + c.Abort() + return + } + + res, err := r.Request("POST", capEndpoint, strings.NewReader(string(requestData))). + CheckHTTPResponse(http.StatusOK). + GetResponse() + if err != nil { + l.Warning("Cap verification failed: %s", err) + c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", err)) + c.Abort() + return + } + + var capRes capResponse + err = json.Unmarshal([]byte(res), &capRes) + if err != nil { + l.Warning("Cap verification failed: %s", err) + c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", err)) + c.Abort() + return + } + + if !capRes.Success { + l.Warning("Cap verification failed: validation returned false") + c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", nil)) + c.Abort() + return + } + break } } diff --git a/pkg/setting/provider.go b/pkg/setting/provider.go index 4cc4aa07..2b379222 100644 --- a/pkg/setting/provider.go +++ b/pkg/setting/provider.go @@ -38,6 +38,8 @@ type ( TcCaptcha(ctx context.Context) *TcCaptcha // TurnstileCaptcha returns the Cloudflare Turnstile settings. TurnstileCaptcha(ctx context.Context) *Turnstile + // CapCaptcha returns the Cap settings. + CapCaptcha(ctx context.Context) *Cap // EmailActivationEnabled returns true if email activation is required. EmailActivationEnabled(ctx context.Context) bool // DefaultGroup returns the default group ID for new users. @@ -638,6 +640,14 @@ func (s *settingProvider) TurnstileCaptcha(ctx context.Context) *Turnstile { } } +func (s *settingProvider) CapCaptcha(ctx context.Context) *Cap { + return &Cap{ + InstanceURL: s.getString(ctx, "captcha_cap_instance_url", ""), + KeyID: s.getString(ctx, "captcha_cap_key_id", ""), + KeySecret: s.getString(ctx, "captcha_cap_key_secret", ""), + } +} + func (s *settingProvider) ReCaptcha(ctx context.Context) *ReCaptcha { return &ReCaptcha{ Secret: s.getString(ctx, "captcha_ReCaptchaSecret", ""), diff --git a/pkg/setting/types.go b/pkg/setting/types.go index b4c685da..d3e4d5ef 100644 --- a/pkg/setting/types.go +++ b/pkg/setting/types.go @@ -28,6 +28,7 @@ const ( CaptchaReCaptcha = CaptchaType("recaptcha") CaptchaTcaptcha = CaptchaType("tcaptcha") CaptchaTurnstile = CaptchaType("turnstile") + CaptchaCap = CaptchaType("cap") ) type ReCaptcha struct { @@ -47,6 +48,12 @@ type Turnstile struct { Secret string } +type Cap struct { + InstanceURL string + KeyID string + KeySecret string +} + type SMTP struct { FromName string From string diff --git a/service/basic/site.go b/service/basic/site.go index 814cfea3..d291a256 100644 --- a/service/basic/site.go +++ b/service/basic/site.go @@ -28,6 +28,8 @@ type SiteConfig struct { ReCaptchaKey string `json:"captcha_ReCaptchaKey,omitempty"` CaptchaType setting.CaptchaType `json:"captcha_type,omitempty"` TurnstileSiteID string `json:"turnstile_site_id,omitempty"` + CapInstanceURL string `json:"captcha_cap_instance_url,omitempty"` + CapKeyID string `json:"captcha_cap_key_id,omitempty"` RegisterEnabled bool `json:"register_enabled,omitempty"` TosUrl string `json:"tos_url,omitempty"` PrivacyPolicyUrl string `json:"privacy_policy_url,omitempty"` @@ -119,6 +121,7 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) { userRes := user.BuildUser(u, dep.HashIDEncoder()) logo := settings.Logo(c) reCaptcha := settings.ReCaptcha(c) + capCaptcha := settings.CapCaptcha(c) appSetting := settings.AppSetting(c) return &SiteConfig{ @@ -132,6 +135,8 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) { CaptchaType: settings.CaptchaType(c), TurnstileSiteID: settings.TurnstileCaptcha(c).Key, ReCaptchaKey: reCaptcha.Key, + CapInstanceURL: capCaptcha.InstanceURL, + CapKeyID: capCaptcha.KeyID, AppPromotion: appSetting.Promotion, }, nil } From 3de33aeb10d1d1e4bbc6e1c996d3e6197cbfcb27 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Fri, 20 Jun 2025 14:07:03 +0800 Subject: [PATCH 02/27] update submodule --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index ededea6c..0720c1a8 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit ededea6c45922365d92c2bf576fb1c25e632594d +Subproject commit 0720c1a80069c6a0c14fce93efbc03417f249a67 From bdc0aafab04df79f56cb6d422d63c6da8436a29f Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Sat, 21 Jun 2025 09:51:12 +0800 Subject: [PATCH 03/27] fix(remote download): file path slashes incorrectly formated for remote download transfer if master and slave node use different path style (#2532) --- assets | 2 +- pkg/filemanager/workflows/remote_download.go | 2 +- pkg/filemanager/workflows/upload.go | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/assets b/assets index 0720c1a8..93d616e7 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 0720c1a80069c6a0c14fce93efbc03417f249a67 +Subproject commit 93d616e742d95ed666938d59200b872e98005527 diff --git a/pkg/filemanager/workflows/remote_download.go b/pkg/filemanager/workflows/remote_download.go index 7b06178d..e884391d 100644 --- a/pkg/filemanager/workflows/remote_download.go +++ b/pkg/filemanager/workflows/remote_download.go @@ -319,7 +319,7 @@ func (m *RemoteDownloadTask) slaveTransfer(ctx context.Context, dep dependency.D } dst := dstUri.JoinRaw(f.Name) - src := filepath.FromSlash(path.Join(m.state.Status.SavePath, f.Name)) + src := path.Join(m.state.Status.SavePath, f.Name) payload.Files = append(payload.Files, SlaveUploadEntity{ Src: src, Uri: dst, diff --git a/pkg/filemanager/workflows/upload.go b/pkg/filemanager/workflows/upload.go index 01841785..65b36ee9 100644 --- a/pkg/filemanager/workflows/upload.go +++ b/pkg/filemanager/workflows/upload.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "os" + "path" "path/filepath" "sync" "sync/atomic" @@ -127,11 +128,11 @@ func (t *SlaveUploadTask) Do(ctx context.Context) (task.Status, error) { t.progress[progressKey] = &queue.Progress{Identifier: file.Uri.String(), Total: file.Size} t.Unlock() - handle, err := os.Open(file.Src) + 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) - ae.Add(filepath.Base(file.Src), fmt.Errorf("failed to open file: %w", err)) + ae.Add(path.Base(file.Src), fmt.Errorf("failed to open file: %w", err)) return } @@ -140,7 +141,7 @@ func (t *SlaveUploadTask) Do(ctx context.Context) (task.Status, error) { 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) - ae.Add(filepath.Base(file.Src), fmt.Errorf("failed to get file stat: %w", err)) + ae.Add(path.Base(file.Src), fmt.Errorf("failed to get file stat: %w", err)) return } @@ -163,7 +164,7 @@ func (t *SlaveUploadTask) Do(ctx context.Context) (task.Status, error) { handle.Close() t.l.Warning("Failed to upload file %s: %s", file.Src, err.Error()) atomic.AddInt64(&t.progress[ProgressTypeUpload].Current, file.Size) - ae.Add(filepath.Base(file.Src), fmt.Errorf("failed to upload file: %w", err)) + ae.Add(path.Base(file.Src), fmt.Errorf("failed to upload file: %w", err)) return } From 8fe28897722834f8c3c07df42eff62e9c0920c01 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Sat, 21 Jun 2025 12:03:08 +0800 Subject: [PATCH 04/27] feat(file apps): add excalidraw (#2317) --- assets | 2 +- inventory/setting.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets b/assets index 93d616e7..44a696a2 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 93d616e742d95ed666938d59200b872e98005527 +Subproject commit 44a696a2e7271bb6b98424670fe93c3df4ebc10b diff --git a/inventory/setting.go b/inventory/setting.go index 9bf9b2f3..cae67151 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -233,7 +233,7 @@ var DefaultSettings = map[string]string{ "site_logo_light": "/static/img/logo_light.svg", "tos_url": "https://cloudreve.org/privacy-policy", "privacy_policy_url": "https://cloudreve.org/privacy-policy", - "explorer_icons": `[{"exts":["mp3","flac","ape","wav","acc","ogg","m4a"],"icon":"audio","color":"#651fff"},{"exts":["m3u8","mp4","flv","avi","wmv","mkv","rm","rmvb","mov","ogv"],"icon":"video","color":"#d50000"},{"exts":["bmp","iff","png","gif","jpg","jpeg","psd","svg","webp","heif","heic","tiff","avif"],"icon":"image","color":"#d32f2f"},{"exts":["3fr","ari","arw","bay","braw","crw","cr2","cr3","cap","dcs","dcr","dng","drf","eip","erf","fff","gpr","iiq","k25","kdc","mdc","mef","mos","mrw","nef","nrw","obm","orf","pef","ptx","pxn","r3d","raf","raw","rwl","rw2","rwz","sr2","srf","srw","tif","x3f"],"icon":"raw","color":"#d32f2f"},{"exts":["pdf"],"color":"#f44336","icon":"pdf"},{"exts":["doc","docx"],"color":"#538ce5","icon":"word"},{"exts":["ppt","pptx"],"color":"#EF633F","icon":"ppt"},{"exts":["xls","xlsx","csv"],"color":"#4caf50","icon":"excel"},{"exts":["txt","html"],"color":"#607d8b","icon":"text"},{"exts":["torrent"],"color":"#5c6bc0","icon":"torrent"},{"exts":["zip","gz","xz","tar","rar","7z","bz2","z"],"color":"#f9a825","icon":"zip"},{"exts":["exe","msi"],"color":"#1a237e","icon":"exe"},{"exts":["apk"],"color":"#8bc34a","icon":"android"},{"exts":["go"],"color":"#16b3da","icon":"go"},{"exts":["py"],"color":"#3776ab","icon":"python"},{"exts":["c"],"color":"#a4c639","icon":"c"},{"exts":["cpp"],"color":"#f34b7d","icon":"cpp"},{"exts":["js","jsx"],"color":"#f4d003","icon":"js"},{"exts":["epub"],"color":"#81b315","icon":"book"},{"exts":["rs"],"color":"#000","color_dark":"#fff","icon":"rust"},{"exts":["drawio"],"color":"#F08705","icon":"flowchart"},{"exts":["dwb"],"color":"#F08705","icon":"whiteboard"},{"exts":["md"],"color":"#383838","color_dark":"#cbcbcb","icon":"markdown"}]`, + "explorer_icons": `[{"exts":["mp3","flac","ape","wav","acc","ogg","m4a"],"icon":"audio","color":"#651fff"},{"exts":["m3u8","mp4","flv","avi","wmv","mkv","rm","rmvb","mov","ogv"],"icon":"video","color":"#d50000"},{"exts":["bmp","iff","png","gif","jpg","jpeg","psd","svg","webp","heif","heic","tiff","avif"],"icon":"image","color":"#d32f2f"},{"exts":["3fr","ari","arw","bay","braw","crw","cr2","cr3","cap","dcs","dcr","dng","drf","eip","erf","fff","gpr","iiq","k25","kdc","mdc","mef","mos","mrw","nef","nrw","obm","orf","pef","ptx","pxn","r3d","raf","raw","rwl","rw2","rwz","sr2","srf","srw","tif","x3f"],"icon":"raw","color":"#d32f2f"},{"exts":["pdf"],"color":"#f44336","icon":"pdf"},{"exts":["doc","docx"],"color":"#538ce5","icon":"word"},{"exts":["ppt","pptx"],"color":"#EF633F","icon":"ppt"},{"exts":["xls","xlsx","csv"],"color":"#4caf50","icon":"excel"},{"exts":["txt","html"],"color":"#607d8b","icon":"text"},{"exts":["torrent"],"color":"#5c6bc0","icon":"torrent"},{"exts":["zip","gz","xz","tar","rar","7z","bz2","z"],"color":"#f9a825","icon":"zip"},{"exts":["exe","msi"],"color":"#1a237e","icon":"exe"},{"exts":["apk"],"color":"#8bc34a","icon":"android"},{"exts":["go"],"color":"#16b3da","icon":"go"},{"exts":["py"],"color":"#3776ab","icon":"python"},{"exts":["c"],"color":"#a4c639","icon":"c"},{"exts":["cpp"],"color":"#f34b7d","icon":"cpp"},{"exts":["js","jsx"],"color":"#f4d003","icon":"js"},{"exts":["epub"],"color":"#81b315","icon":"book"},{"exts":["rs"],"color":"#000","color_dark":"#fff","icon":"rust"},{"exts":["drawio"],"color":"#F08705","icon":"flowchart"},{"exts":["dwb"],"color":"#F08705","icon":"whiteboard"},{"exts":["md"],"color":"#383838","color_dark":"#cbcbcb","icon":"markdown"},{"img":"/static/img/viewers/excalidraw.svg","exts":["excalidraw"]}]`, "explorer_category_image_query": "type=file&case_folding&use_or&name=*.bmp&name=*.iff&name=*.png&name=*.gif&name=*.jpg&name=*.jpeg&name=*.psd&name=*.svg&name=*.webp&name=*.heif&name=*.heic&name=*.tiff&name=*.avif&name=*.3fr&name=*.ari&name=*.arw&name=*.bay&name=*.braw&name=*.crw&name=*.cr2&name=*.cr3&name=*.cap&name=*.dcs&name=*.dcr&name=*.dng&name=*.drf&name=*.eip&name=*.erf&name=*.fff&name=*.gpr&name=*.iiq&name=*.k25&name=*.kdc&name=*.mdc&name=*.mef&name=*.mos&name=*.mrw&name=*.nef&name=*.nrw&name=*.obm&name=*.orf&name=*.pef&name=*.ptx&name=*.pxn&name=*.r3d&name=*.raf&name=*.raw&name=*.rwl&name=*.rw2&name=*.rwz&name=*.sr2&name=*.srf&name=*.srw&name=*.tif&name=*.x3f", "explorer_category_video_query": "type=file&case_folding&use_or&name=*.mp4&name=*.m3u8&name=*.flv&name=*.avi&name=*.wmv&name=*.mkv&name=*.rm&name=*.rmvb&name=*.mov&name=*.ogv", "explorer_category_audio_query": "type=file&case_folding&use_or&name=*.mp3&name=*.flac&name=*.ape&name=*.wav&name=*.acc&name=*.ogg&name=*.m4a", @@ -243,7 +243,7 @@ var DefaultSettings = map[string]string{ "map_provider": "openstreetmap", "map_google_tile_type": "regular", "mime_mapping": `{".xlsx":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",".xltx":"application/vnd.openxmlformats-officedocument.spreadsheetml.template",".potx":"application/vnd.openxmlformats-officedocument.presentationml.template",".ppsx":"application/vnd.openxmlformats-officedocument.presentationml.slideshow",".pptx":"application/vnd.openxmlformats-officedocument.presentationml.presentation",".sldx":"application/vnd.openxmlformats-officedocument.presentationml.slide",".docx":"application/vnd.openxmlformats-officedocument.wordprocessingml.document",".dotx":"application/vnd.openxmlformats-officedocument.wordprocessingml.template",".xlam":"application/vnd.ms-excel.addin.macroEnabled.12",".xlsb":"application/vnd.ms-excel.sheet.binary.macroEnabled.12",".apk":"application/vnd.android.package-archive",".hqx":"application/mac-binhex40",".cpt":"application/mac-compactpro",".doc":"application/msword",".ogg":"application/ogg",".pdf":"application/pdf",".rtf":"text/rtf",".mif":"application/vnd.mif",".xls":"application/vnd.ms-excel",".ppt":"application/vnd.ms-powerpoint",".odc":"application/vnd.oasis.opendocument.chart",".odb":"application/vnd.oasis.opendocument.database",".odf":"application/vnd.oasis.opendocument.formula",".odg":"application/vnd.oasis.opendocument.graphics",".otg":"application/vnd.oasis.opendocument.graphics-template",".odi":"application/vnd.oasis.opendocument.image",".odp":"application/vnd.oasis.opendocument.presentation",".otp":"application/vnd.oasis.opendocument.presentation-template",".ods":"application/vnd.oasis.opendocument.spreadsheet",".ots":"application/vnd.oasis.opendocument.spreadsheet-template",".odt":"application/vnd.oasis.opendocument.text",".odm":"application/vnd.oasis.opendocument.text-master",".ott":"application/vnd.oasis.opendocument.text-template",".oth":"application/vnd.oasis.opendocument.text-web",".sxw":"application/vnd.sun.xml.writer",".stw":"application/vnd.sun.xml.writer.template",".sxc":"application/vnd.sun.xml.calc",".stc":"application/vnd.sun.xml.calc.template",".sxd":"application/vnd.sun.xml.draw",".std":"application/vnd.sun.xml.draw.template",".sxi":"application/vnd.sun.xml.impress",".sti":"application/vnd.sun.xml.impress.template",".sxg":"application/vnd.sun.xml.writer.global",".sxm":"application/vnd.sun.xml.math",".sis":"application/vnd.symbian.install",".wbxml":"application/vnd.wap.wbxml",".wmlc":"application/vnd.wap.wmlc",".wmlsc":"application/vnd.wap.wmlscriptc",".bcpio":"application/x-bcpio",".torrent":"application/x-bittorrent",".bz2":"application/x-bzip2",".vcd":"application/x-cdlink",".pgn":"application/x-chess-pgn",".cpio":"application/x-cpio",".csh":"application/x-csh",".dvi":"application/x-dvi",".spl":"application/x-futuresplash",".gtar":"application/x-gtar",".hdf":"application/x-hdf",".jar":"application/x-java-archive",".jnlp":"application/x-java-jnlp-file",".js":"application/x-javascript",".ksp":"application/x-kspread",".chrt":"application/x-kchart",".kil":"application/x-killustrator",".latex":"application/x-latex",".rpm":"application/x-rpm",".sh":"application/x-sh",".shar":"application/x-shar",".swf":"application/x-shockwave-flash",".sit":"application/x-stuffit",".sv4cpio":"application/x-sv4cpio",".sv4crc":"application/x-sv4crc",".tar":"application/x-tar",".tcl":"application/x-tcl",".tex":"application/x-tex",".man":"application/x-troff-man",".me":"application/x-troff-me",".ms":"application/x-troff-ms",".ustar":"application/x-ustar",".src":"application/x-wais-source",".zip":"application/zip",".m3u":"audio/x-mpegurl",".ra":"audio/x-pn-realaudio",".wav":"audio/x-wav",".wma":"audio/x-ms-wma",".wax":"audio/x-ms-wax",".pdb":"chemical/x-pdb",".xyz":"chemical/x-xyz",".bmp":"image/bmp",".gif":"image/gif",".ief":"image/ief",".png":"image/png",".wbmp":"image/vnd.wap.wbmp",".ras":"image/x-cmu-raster",".pnm":"image/x-portable-anymap",".pbm":"image/x-portable-bitmap",".pgm":"image/x-portable-graymap",".ppm":"image/x-portable-pixmap",".rgb":"image/x-rgb",".xbm":"image/x-xbitmap",".xpm":"image/x-xpixmap",".xwd":"image/x-xwindowdump",".css":"text/css",".rtx":"text/richtext",".tsv":"text/tab-separated-values",".jad":"text/vnd.sun.j2me.app-descriptor",".wml":"text/vnd.wap.wml",".wmls":"text/vnd.wap.wmlscript",".etx":"text/x-setext",".mxu":"video/vnd.mpegurl",".flv":"video/x-flv",".wm":"video/x-ms-wm",".wmv":"video/x-ms-wmv",".wmx":"video/x-ms-wmx",".wvx":"video/x-ms-wvx",".avi":"video/x-msvideo",".movie":"video/x-sgi-movie",".ice":"x-conference/x-cooltalk",".3gp":"video/3gpp",".ai":"application/postscript",".aif":"audio/x-aiff",".aifc":"audio/x-aiff",".aiff":"audio/x-aiff",".asc":"text/plain",".atom":"application/atom+xml",".au":"audio/basic",".bin":"application/octet-stream",".cdf":"application/x-netcdf",".cgm":"image/cgm",".class":"application/octet-stream",".dcr":"application/x-director",".dif":"video/x-dv",".dir":"application/x-director",".djv":"image/vnd.djvu",".djvu":"image/vnd.djvu",".dll":"application/octet-stream",".dmg":"application/octet-stream",".dms":"application/octet-stream",".dtd":"application/xml-dtd",".dv":"video/x-dv",".dxr":"application/x-director",".eps":"application/postscript",".exe":"application/octet-stream",".ez":"application/andrew-inset",".gram":"application/srgs",".grxml":"application/srgs+xml",".gz":"application/x-gzip",".htm":"text/html",".html":"text/html",".ico":"image/x-icon",".ics":"text/calendar",".ifb":"text/calendar",".iges":"model/iges",".igs":"model/iges",".jp2":"image/jp2",".jpe":"image/jpeg",".jpeg":"image/jpeg",".jpg":"image/jpeg",".kar":"audio/midi",".lha":"application/octet-stream",".lzh":"application/octet-stream",".m4a":"audio/mp4a-latm",".m4p":"audio/mp4a-latm",".m4u":"video/vnd.mpegurl",".m4v":"video/x-m4v",".mac":"image/x-macpaint",".mathml":"application/mathml+xml",".mesh":"model/mesh",".mid":"audio/midi",".midi":"audio/midi",".mov":"video/quicktime",".mp2":"audio/mpeg",".mp3":"audio/mpeg",".mp4":"video/mp4",".mpe":"video/mpeg",".mpeg":"video/mpeg",".mpg":"video/mpeg",".mpga":"audio/mpeg",".msh":"model/mesh",".nc":"application/x-netcdf",".oda":"application/oda",".ogv":"video/ogv",".pct":"image/pict",".pic":"image/pict",".pict":"image/pict",".pnt":"image/x-macpaint",".pntg":"image/x-macpaint",".ps":"application/postscript",".qt":"video/quicktime",".qti":"image/x-quicktime",".qtif":"image/x-quicktime",".ram":"audio/x-pn-realaudio",".rdf":"application/rdf+xml",".rm":"application/vnd.rn-realmedia",".roff":"application/x-troff",".sgm":"text/sgml",".sgml":"text/sgml",".silo":"model/mesh",".skd":"application/x-koan",".skm":"application/x-koan",".skp":"application/x-koan",".skt":"application/x-koan",".smi":"application/smil",".smil":"application/smil",".snd":"audio/basic",".so":"application/octet-stream",".svg":"image/svg+xml",".t":"application/x-troff",".texi":"application/x-texinfo",".texinfo":"application/x-texinfo",".tif":"image/tiff",".tiff":"image/tiff",".tr":"application/x-troff",".txt":"text/plain; charset=utf-8",".vrml":"model/vrml",".vxml":"application/voicexml+xml",".webm":"video/webm",".wrl":"model/vrml",".xht":"application/xhtml+xml",".xhtml":"application/xhtml+xml",".xml":"application/xml",".xsl":"application/xml",".xslt":"application/xslt+xml",".xul":"application/vnd.mozilla.xul+xml",".webp":"image/webp",".323":"text/h323",".aab":"application/x-authoware-bin",".aam":"application/x-authoware-map",".aas":"application/x-authoware-seg",".acx":"application/internet-property-stream",".als":"audio/X-Alpha5",".amc":"application/x-mpeg",".ani":"application/octet-stream",".asd":"application/astound",".asf":"video/x-ms-asf",".asn":"application/astound",".asp":"application/x-asap",".asr":"video/x-ms-asf",".asx":"video/x-ms-asf",".avb":"application/octet-stream",".awb":"audio/amr-wb",".axs":"application/olescript",".bas":"text/plain",".bin ":"application/octet-stream",".bld":"application/bld",".bld2":"application/bld2",".bpk":"application/octet-stream",".c":"text/plain",".cal":"image/x-cals",".cat":"application/vnd.ms-pkiseccat",".ccn":"application/x-cnc",".cco":"application/x-cocoa",".cer":"application/x-x509-ca-cert",".cgi":"magnus-internal/cgi",".chat":"application/x-chat",".clp":"application/x-msclip",".cmx":"image/x-cmx",".co":"application/x-cult3d-object",".cod":"image/cis-cod",".conf":"text/plain",".cpp":"text/plain",".crd":"application/x-mscardfile",".crl":"application/pkix-crl",".crt":"application/x-x509-ca-cert",".csm":"chemical/x-csml",".csml":"chemical/x-csml",".cur":"application/octet-stream",".dcm":"x-lml/x-evm",".dcx":"image/x-dcx",".der":"application/x-x509-ca-cert",".dhtml":"text/html",".dot":"application/msword",".dwf":"drawing/x-dwf",".dwg":"application/x-autocad",".dxf":"application/x-autocad",".ebk":"application/x-expandedbook",".emb":"chemical/x-embl-dl-nucleotide",".embl":"chemical/x-embl-dl-nucleotide",".epub":"application/epub+zip",".eri":"image/x-eri",".es":"audio/echospeech",".esl":"audio/echospeech",".etc":"application/x-earthtime",".evm":"x-lml/x-evm",".evy":"application/envoy",".fh4":"image/x-freehand",".fh5":"image/x-freehand",".fhc":"image/x-freehand",".fif":"application/fractals",".flr":"x-world/x-vrml",".fm":"application/x-maker",".fpx":"image/x-fpx",".fvi":"video/isivideo",".gau":"chemical/x-gaussian-input",".gca":"application/x-gca-compressed",".gdb":"x-lml/x-gdb",".gps":"application/x-gps",".h":"text/plain",".hdm":"text/x-hdml",".hdml":"text/x-hdml",".hlp":"application/winhlp",".hta":"application/hta",".htc":"text/x-component",".hts":"text/html",".htt":"text/webviewhtml",".ifm":"image/gif",".ifs":"image/ifs",".iii":"application/x-iphone",".imy":"audio/melody",".ins":"application/x-internet-signup",".ips":"application/x-ipscript",".ipx":"application/x-ipix",".isp":"application/x-internet-signup",".it":"audio/x-mod",".itz":"audio/x-mod",".ivr":"i-world/i-vrml",".j2k":"image/j2k",".jam":"application/x-jam",".java":"text/plain",".jfif":"image/pipeg",".jpz":"image/jpeg",".jwc":"application/jwc",".kjx":"application/x-kjx",".lak":"x-lml/x-lak",".lcc":"application/fastman",".lcl":"application/x-digitalloca",".lcr":"application/x-digitalloca",".lgh":"application/lgh",".lml":"x-lml/x-lml",".lmlpack":"x-lml/x-lmlpack",".log":"text/plain",".lsf":"video/x-la-asf",".lsx":"video/x-la-asf",".m13":"application/x-msmediaview",".m14":"application/x-msmediaview",".m15":"audio/x-mod",".m3url":"audio/x-mpegurl",".m4b":"audio/mp4a-latm",".ma1":"audio/ma1",".ma2":"audio/ma2",".ma3":"audio/ma3",".ma5":"audio/ma5",".map":"magnus-internal/imagemap",".mbd":"application/mbedlet",".mct":"application/x-mascot",".mdb":"application/x-msaccess",".mdz":"audio/x-mod",".mel":"text/x-vmel",".mht":"message/rfc822",".mhtml":"message/rfc822",".mi":"application/x-mif",".mil":"image/x-cals",".mio":"audio/x-mio",".mmf":"application/x-skt-lbs",".mng":"video/x-mng",".mny":"application/x-msmoney",".moc":"application/x-mocha",".mocha":"application/x-mocha",".mod":"audio/x-mod",".mof":"application/x-yumekara",".mol":"chemical/x-mdl-molfile",".mop":"chemical/x-mopac-input",".mpa":"video/mpeg",".mpc":"application/vnd.mpohun.certificate",".mpg4":"video/mp4",".mpn":"application/vnd.mophun.application",".mpp":"application/vnd.ms-project",".mps":"application/x-mapserver",".mpv2":"video/mpeg",".mrl":"text/x-mrml",".mrm":"application/x-mrm",".msg":"application/vnd.ms-outlook",".mts":"application/metastream",".mtx":"application/metastream",".mtz":"application/metastream",".mvb":"application/x-msmediaview",".mzv":"application/metastream",".nar":"application/zip",".nbmp":"image/nbmp",".ndb":"x-lml/x-ndb",".ndwn":"application/ndwn",".nif":"application/x-nif",".nmz":"application/x-scream",".nokia-op-logo":"image/vnd.nok-oplogo-color",".npx":"application/x-netfpx",".nsnd":"audio/nsnd",".nva":"application/x-neva1",".nws":"message/rfc822",".oom":"application/x-AtlasMate-Plugin",".p10":"application/pkcs10",".p12":"application/x-pkcs12",".p7b":"application/x-pkcs7-certificates",".p7c":"application/x-pkcs7-mime",".p7m":"application/x-pkcs7-mime",".p7r":"application/x-pkcs7-certreqresp",".p7s":"application/x-pkcs7-signature",".pac":"audio/x-pac",".pae":"audio/x-epac",".pan":"application/x-pan",".pcx":"image/x-pcx",".pda":"image/x-pda",".pfr":"application/font-tdpfr",".pfx":"application/x-pkcs12",".pko":"application/ynd.ms-pkipko",".pm":"application/x-perl",".pma":"application/x-perfmon",".pmc":"application/x-perfmon",".pmd":"application/x-pmd",".pml":"application/x-perfmon",".pmr":"application/x-perfmon",".pmw":"application/x-perfmon",".pnz":"image/png",".pot,":"application/vnd.ms-powerpoint",".pps":"application/vnd.ms-powerpoint",".pqf":"application/x-cprplayer",".pqi":"application/cprplayer",".prc":"application/x-prc",".prf":"application/pics-rules",".prop":"text/plain",".proxy":"application/x-ns-proxy-autoconfig",".ptlk":"application/listenup",".pub":"application/x-mspublisher",".pvx":"video/x-pv-pvx",".qcp":"audio/vnd.qcelp",".r3t":"text/vnd.rn-realtext3d",".rar":"application/octet-stream",".rc":"text/plain",".rf":"image/vnd.rn-realflash",".rlf":"application/x-richlink",".rmf":"audio/x-rmf",".rmi":"audio/mid",".rmm":"audio/x-pn-realaudio",".rmvb":"audio/x-pn-realaudio",".rnx":"application/vnd.rn-realplayer",".rp":"image/vnd.rn-realpix",".rt":"text/vnd.rn-realtext",".rte":"x-lml/x-gps",".rtg":"application/metastream",".rv":"video/vnd.rn-realvideo",".rwc":"application/x-rogerwilco",".s3m":"audio/x-mod",".s3z":"audio/x-mod",".sca":"application/x-supercard",".scd":"application/x-msschedule",".sct":"text/scriptlet",".sdf":"application/e-score",".sea":"application/x-stuffit",".setpay":"application/set-payment_old-initiation",".setreg":"application/set-registration-initiation",".shtml":"text/html",".shtm":"text/html",".shw":"application/presentations",".si6":"image/si6",".si7":"image/vnd.stiwap.sis",".si9":"image/vnd.lgtwap.sis",".slc":"application/x-salsa",".smd":"audio/x-smd",".smp":"application/studiom",".smz":"audio/x-smd",".spc":"application/x-pkcs7-certificates",".spr":"application/x-sprite",".sprite":"application/x-sprite",".sdp":"application/sdp",".spt":"application/x-spt",".sst":"application/vnd.ms-pkicertstore",".stk":"application/hyperstudio",".stl":"application/vnd.ms-pkistl",".stm":"text/html",".svf":"image/vnd",".svh":"image/svh",".svr":"x-world/x-svr",".swfl":"application/x-shockwave-flash",".tad":"application/octet-stream",".talk":"text/x-speech",".taz":"application/x-tar",".tbp":"application/x-timbuktu",".tbt":"application/x-timbuktu",".tgz":"application/x-compressed",".thm":"application/vnd.eri.thm",".tki":"application/x-tkined",".tkined":"application/x-tkined",".toc":"application/toc",".toy":"image/toy",".trk":"x-lml/x-gps",".trm":"application/x-msterminal",".tsi":"audio/tsplayer",".tsp":"application/dsptype",".ttf":"application/octet-stream",".ttz":"application/t-time",".uls":"text/iuls",".ult":"audio/x-mod",".uu":"application/x-uuencode",".uue":"application/x-uuencode",".vcf":"text/x-vcard",".vdo":"video/vdo",".vib":"audio/vib",".viv":"video/vivo",".vivo":"video/vivo",".vmd":"application/vocaltec-media-desc",".vmf":"application/vocaltec-media-file",".vmi":"application/x-dreamcast-vms-info",".vms":"application/x-dreamcast-vms",".vox":"audio/voxware",".vqe":"audio/x-twinvq-plugin",".vqf":"audio/x-twinvq",".vql":"audio/x-twinvq",".vre":"x-world/x-vream",".vrt":"x-world/x-vrt",".vrw":"x-world/x-vream",".vts":"workbook/formulaone",".wcm":"application/vnd.ms-works",".wdb":"application/vnd.ms-works",".web":"application/vnd.xara",".wi":"image/wavelet",".wis":"application/x-InstallShield",".wks":"application/vnd.ms-works",".wmd":"application/x-ms-wmd",".wmf":"application/x-msmetafile",".wmlscript":"text/vnd.wap.wmlscript",".wmz":"application/x-ms-wmz",".wpng":"image/x-up-wpng",".wps":"application/vnd.ms-works",".wpt":"x-lml/x-gps",".wri":"application/x-mswrite",".wrz":"x-world/x-vrml",".ws":"text/vnd.wap.wmlscript",".wsc":"application/vnd.wap.wmlscriptc",".wv":"video/wavelet",".wxl":"application/x-wxl",".x-gzip":"application/x-gzip",".xaf":"x-world/x-vrml",".xar":"application/vnd.xara",".xdm":"application/x-xdma",".xdma":"application/x-xdma",".xdw":"application/vnd.fujixerox.docuworks",".xhtm":"application/xhtml+xml",".xla":"application/vnd.ms-excel",".xlc":"application/vnd.ms-excel",".xll":"application/x-excel",".xlm":"application/vnd.ms-excel",".xlt":"application/vnd.ms-excel",".xlw":"application/vnd.ms-excel",".xm":"audio/x-mod",".xmz":"audio/x-mod",".xof":"x-world/x-vrml",".xpi":"application/x-xpinstall",".xsit":"text/xml",".yz1":"application/x-yz1",".z":"application/x-compress",".zac":"application/x-zaurus-zac",".json":"application/json"}`, - "file_viewers": `[{"viewers":[{"id":"music","type":"builtin","action":"view","display_name":"fileManager.musicPlayer","exts":["mp3","ogg","wav","flac","m4a"]},{"id":"epub","type":"builtin","action":"view","display_name":"fileManager.epubViewer","exts":["epub"]},{"id":"googledocs","type":"custom","action":"view","display_name":"fileManager.googledocs","icon":"/static/img/viewers/gdrive.png","url":"https://docs.google.com/gview?url={$src}&embedded=true","exts":["jpeg","png","gif","tiff","bmp","webm","mpeg4","3gpp","mov","avi","mpegps","wmv","flv","txt","css","html","php","c","cpp","h","hpp","js","doc","docx","xls","xlsx","ppt","pptx","pdf","pages","ai","psd","tiff","dxf","svg","eps","ps","ttf","xps"],"max_size":26214400},{"id":"m365online","type":"custom","action":"view","display_name":"fileManager.m365viewer","icon":"/static/img/viewers/m365.svg","url":"https://view.officeapps.live.com/op/view.aspx?src={$src}","exts":["doc","docx","docm","dotm","dotx","xlsx","xlsb","xls","xlsm","pptx","ppsx","ppt","pps","pptm","potm","ppam","potx","ppsm"],"max_size":10485760},{"id":"pdf","type":"builtin","action":"view","display_name":"fileManager.pdfViewer","exts":["pdf"]},{"id":"video","type":"builtin","action":"view","icon":"/static/img/viewers/artplayer.png","display_name":"Artplayer","exts":["mp4","mkv","webm","avi","m3u8","mov","flv"]},{"id":"markdown","type":"builtin","action":"edit","display_name":"fileManager.markdownEditor","exts":["md"],"templates":[{"ext":"md","display_name":"Markdown"}]},{"id":"drawio","type":"builtin","action":"edit","icon":"/static/img/viewers/drawio.svg","display_name":"draw.io","exts":["drawio","dwb"],"props":{"host":"https://embed.diagrams.net"},"templates":[{"ext":"drawio","display_name":"fileManager.diagram"},{"ext":"dwb","display_name":"fileManager.whiteboard"}]},{"id":"image","type":"builtin","action":"edit","display_name":"fileManager.imageViewer","exts":["bmp","png","gif","jpg","jpeg","svg","webp","heic","heif"]},{"id":"monaco","type":"builtin","action":"edit","icon":"/static/img/viewers/monaco.svg","display_name":"fileManager.monacoEditor","exts":["md","txt","json","php","py","bat","c","h","cpp","hpp","cs","css","dockerfile","go","html","htm","ini","java","js","jsx","less","lua","sh","sql","xml","yaml"],"templates":[{"ext":"txt","display_name":"fileManager.text"}]},{"id":"photopea","type":"builtin","icon":"/static/img/viewers/photopea.png","action":"edit","display_name":"Photopea","exts":["psd","ai","indd","xcf","xd","fig","kri","clip","pxd","pxz","cdr","ufo","afphoyo","svg","esp","pdf","pdn","wmf","emf","png","jpg","jpeg","gif","webp","ico","icns","bmp","avif","heic","jxl","ppm","pgm","pbm","tiff","dds","iff","anim","tga","dng","nef","cr2","cr3","arw","rw2","raf","orf","gpr","3fr","fff"]}]}]`, + "file_viewers": `[{"viewers":[{"id":"music","type":"builtin","action":"view","display_name":"fileManager.musicPlayer","exts":["mp3","ogg","wav","flac","m4a"]},{"id":"epub","type":"builtin","action":"view","display_name":"fileManager.epubViewer","exts":["epub"]},{"id":"googledocs","type":"custom","action":"view","display_name":"fileManager.googledocs","icon":"/static/img/viewers/gdrive.png","url":"https://docs.google.com/gview?url={$src}&embedded=true","exts":["jpeg","png","gif","tiff","bmp","webm","mpeg4","3gpp","mov","avi","mpegps","wmv","flv","txt","css","html","php","c","cpp","h","hpp","js","doc","docx","xls","xlsx","ppt","pptx","pdf","pages","ai","psd","tiff","dxf","svg","eps","ps","ttf","xps"],"max_size":26214400},{"id":"m365online","type":"custom","action":"view","display_name":"fileManager.m365viewer","icon":"/static/img/viewers/m365.svg","url":"https://view.officeapps.live.com/op/view.aspx?src={$src}","exts":["doc","docx","docm","dotm","dotx","xlsx","xlsb","xls","xlsm","pptx","ppsx","ppt","pps","pptm","potm","ppam","potx","ppsm"],"max_size":10485760},{"id":"pdf","type":"builtin","action":"view","display_name":"fileManager.pdfViewer","exts":["pdf"]},{"id":"video","type":"builtin","action":"view","icon":"/static/img/viewers/artplayer.png","display_name":"Artplayer","exts":["mp4","mkv","webm","avi","m3u8","mov","flv"]},{"id":"markdown","type":"builtin","action":"edit","display_name":"fileManager.markdownEditor","exts":["md"],"templates":[{"ext":"md","display_name":"Markdown"}]},{"id":"drawio","type":"builtin","action":"edit","icon":"/static/img/viewers/drawio.svg","display_name":"draw.io","exts":["drawio","dwb"],"props":{"host":"https://embed.diagrams.net"},"templates":[{"ext":"drawio","display_name":"fileManager.diagram"},{"ext":"dwb","display_name":"fileManager.whiteboard"}]},{"id":"image","type":"builtin","action":"edit","display_name":"fileManager.imageViewer","exts":["bmp","png","gif","jpg","jpeg","svg","webp","heic","heif"]},{"id":"monaco","type":"builtin","action":"edit","icon":"/static/img/viewers/monaco.svg","display_name":"fileManager.monacoEditor","exts":["md","txt","json","php","py","bat","c","h","cpp","hpp","cs","css","dockerfile","go","html","htm","ini","java","js","jsx","less","lua","sh","sql","xml","yaml"],"templates":[{"ext":"txt","display_name":"fileManager.text"}]},{"id":"photopea","type":"builtin","icon":"/static/img/viewers/photopea.png","action":"edit","display_name":"Photopea","exts":["psd","ai","indd","xcf","xd","fig","kri","clip","pxd","pxz","cdr","ufo","afphoyo","svg","esp","pdf","pdn","wmf","emf","png","jpg","jpeg","gif","webp","ico","icns","bmp","avif","heic","jxl","ppm","pgm","pbm","tiff","dds","iff","anim","tga","dng","nef","cr2","cr3","arw","rw2","raf","orf","gpr","3fr","fff"]},{"id":"excalidraw","type":"builtin","action":"view","icon":"/static/img/viewers/excalidraw.svg","display_name":"Excalidraw","exts":["excalidraw"],"templates":[{"ext":"excalidraw","display_name":"Excalidraw"}]}]}]`, "logto_enabled": "0", "logto_config": `{"direct_sign_in":true,"display_name":"vas.sso"}`, "qq_login": `0`, From fec549f5ec7cd0bccd665561511a421642389c21 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Sun, 22 Jun 2025 10:31:33 +0800 Subject: [PATCH 05/27] feat(ent): migrate DB settings in patches --- application/constants/constants.go | 2 +- go.mod | 1 + go.sum | 2 + inventory/client.go | 307 --------------------- inventory/migration.go | 416 +++++++++++++++++++++++++++++ inventory/setting.go | 266 +++++++++++++++++- inventory/types/types.go | 45 ++++ middleware/wopi.go | 4 +- pkg/filemanager/manager/manager.go | 2 +- pkg/filemanager/manager/viewer.go | 7 +- pkg/setting/provider.go | 9 +- pkg/setting/types.go | 36 --- pkg/wopi/discovery.go | 24 +- pkg/wopi/wopi.go | 5 +- service/admin/tools.go | 3 +- service/basic/site.go | 3 +- service/explorer/viewer.go | 7 +- 17 files changed, 762 insertions(+), 377 deletions(-) create mode 100644 inventory/migration.go diff --git a/application/constants/constants.go b/application/constants/constants.go index 81b96b5f..a50d4153 100644 --- a/application/constants/constants.go +++ b/application/constants/constants.go @@ -3,7 +3,7 @@ package constants // These values will be injected at build time, DO NOT EDIT. // BackendVersion 当前后端版本号 -var BackendVersion = "4.0.0-alpha.1" +var BackendVersion = "4.1.0" // IsPro 是否为Pro版本 var IsPro = "false" diff --git a/go.mod b/go.mod index fb4f2807..1fb75381 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.23.0 require ( entgo.io/ent v0.13.0 + github.com/Masterminds/semver/v3 v3.3.1 github.com/abslant/gzip v0.0.9 github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible github.com/aws/aws-sdk-go v1.31.5 diff --git a/go.sum b/go.sum index e2c97f55..8fe73300 100644 --- a/go.sum +++ b/go.sum @@ -76,6 +76,8 @@ github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF0 github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= diff --git a/inventory/client.go b/inventory/client.go index d93cf52b..6e3d2934 100644 --- a/inventory/client.go +++ b/inventory/client.go @@ -5,20 +5,12 @@ import ( rawsql "database/sql" "database/sql/driver" "fmt" - "os" "time" "entgo.io/ent/dialect/sql" - "github.com/cloudreve/Cloudreve/v4/application/constants" "github.com/cloudreve/Cloudreve/v4/ent" - "github.com/cloudreve/Cloudreve/v4/ent/group" - "github.com/cloudreve/Cloudreve/v4/ent/node" _ "github.com/cloudreve/Cloudreve/v4/ent/runtime" - "github.com/cloudreve/Cloudreve/v4/ent/setting" - "github.com/cloudreve/Cloudreve/v4/ent/storagepolicy" "github.com/cloudreve/Cloudreve/v4/inventory/debug" - "github.com/cloudreve/Cloudreve/v4/inventory/types" - "github.com/cloudreve/Cloudreve/v4/pkg/boolset" "github.com/cloudreve/Cloudreve/v4/pkg/cache" "github.com/cloudreve/Cloudreve/v4/pkg/conf" "github.com/cloudreve/Cloudreve/v4/pkg/logging" @@ -153,302 +145,3 @@ func (d sqlite3Driver) Open(name string) (conn driver.Conn, err error) { func init() { rawsql.Register("sqlite3", sqlite3Driver{Driver: &sqlite.Driver{}}) } - -// needMigration exams if required schema version is satisfied. -func needMigration(client *ent.Client, ctx context.Context, requiredDbVersion string) bool { - c, _ := client.Setting.Query().Where(setting.NameEQ(DBVersionPrefix + requiredDbVersion)).Count(ctx) - return c == 0 -} - -func migrate(l logging.Logger, client *ent.Client, ctx context.Context, kv cache.Driver, requiredDbVersion string) error { - l.Info("Start initializing database schema...") - l.Info("Creating basic table schema...") - if err := client.Schema.Create(ctx); err != nil { - return fmt.Errorf("Failed creating schema resources: %w", err) - } - - migrateDefaultSettings(l, client, ctx, kv) - - if err := migrateDefaultStoragePolicy(l, client, ctx); err != nil { - return fmt.Errorf("failed migrating default storage policy: %w", err) - } - - if err := migrateSysGroups(l, client, ctx); err != nil { - return fmt.Errorf("failed migrating default storage policy: %w", err) - } - - client.Setting.Create().SetName(DBVersionPrefix + requiredDbVersion).SetValue("installed").Save(ctx) - return nil -} - -func migrateDefaultSettings(l logging.Logger, client *ent.Client, ctx context.Context, kv cache.Driver) { - // clean kv cache - if err := kv.DeleteAll(); err != nil { - l.Warning("Failed to remove all KV entries while schema migration: %s", err) - } - - // List existing settings into a map - existingSettings := make(map[string]struct{}) - settings, err := client.Setting.Query().All(ctx) - if err != nil { - l.Warning("Failed to query existing settings: %s", err) - } - - for _, s := range settings { - existingSettings[s.Name] = struct{}{} - } - - l.Info("Insert default settings...") - for k, v := range DefaultSettings { - if _, ok := existingSettings[k]; ok { - l.Debug("Skip inserting setting %s, already exists.", k) - continue - } - - if override, ok := os.LookupEnv(EnvDefaultOverwritePrefix + k); ok { - l.Info("Override default setting %q with env value %q", k, override) - v = override - } - - client.Setting.Create().SetName(k).SetValue(v).SaveX(ctx) - } -} - -func migrateDefaultStoragePolicy(l logging.Logger, client *ent.Client, ctx context.Context) error { - if _, err := client.StoragePolicy.Query().Where(storagepolicy.ID(1)).First(ctx); err == nil { - l.Info("Default storage policy (ID=1) already exists, skip migrating.") - return nil - } - - l.Info("Insert default storage policy...") - if _, err := client.StoragePolicy.Create(). - SetName("Default storage policy"). - SetType(types.PolicyTypeLocal). - SetDirNameRule(util.DataPath("uploads/{uid}/{path}")). - SetFileNameRule("{uid}_{randomkey8}_{originname}"). - SetSettings(&types.PolicySetting{ - ChunkSize: 25 << 20, // 25MB - PreAllocate: true, - }). - Save(ctx); err != nil { - return fmt.Errorf("failed to create default storage policy: %w", err) - } - - return nil -} - -func migrateSysGroups(l logging.Logger, client *ent.Client, ctx context.Context) error { - if err := migrateAdminGroup(l, client, ctx); err != nil { - return err - } - - if err := migrateUserGroup(l, client, ctx); err != nil { - return err - } - - if err := migrateAnonymousGroup(l, client, ctx); err != nil { - return err - } - - if err := migrateMasterNode(l, client, ctx); err != nil { - return err - } - - return nil -} - -func migrateAdminGroup(l logging.Logger, client *ent.Client, ctx context.Context) error { - if _, err := client.Group.Query().Where(group.ID(1)).First(ctx); err == nil { - l.Info("Default admin group (ID=1) already exists, skip migrating.") - return nil - } - - l.Info("Insert default admin group...") - permissions := &boolset.BooleanSet{} - boolset.Sets(map[types.GroupPermission]bool{ - types.GroupPermissionIsAdmin: true, - types.GroupPermissionShare: true, - types.GroupPermissionWebDAV: true, - types.GroupPermissionWebDAVProxy: true, - types.GroupPermissionArchiveDownload: true, - types.GroupPermissionArchiveTask: true, - types.GroupPermissionShareDownload: true, - types.GroupPermissionRemoteDownload: true, - types.GroupPermissionRedirectedSource: true, - types.GroupPermissionAdvanceDelete: true, - types.GroupPermissionIgnoreFileOwnership: true, - // TODO: review default permission - }, permissions) - if _, err := client.Group.Create(). - SetName("Admin"). - SetStoragePoliciesID(1). - SetMaxStorage(1 * constants.TB). // 1 TB default storage - SetPermissions(permissions). - SetSettings(&types.GroupSetting{ - SourceBatchSize: 1000, - Aria2BatchSize: 50, - MaxWalkedFiles: 100000, - TrashRetention: 7 * 24 * 3600, - RedirectedSource: true, - }). - Save(ctx); err != nil { - return fmt.Errorf("failed to create default admin group: %w", err) - } - - return nil -} - -func migrateUserGroup(l logging.Logger, client *ent.Client, ctx context.Context) error { - if _, err := client.Group.Query().Where(group.ID(2)).First(ctx); err == nil { - l.Info("Default user group (ID=2) already exists, skip migrating.") - return nil - } - - l.Info("Insert default user group...") - permissions := &boolset.BooleanSet{} - boolset.Sets(map[types.GroupPermission]bool{ - types.GroupPermissionShare: true, - types.GroupPermissionShareDownload: true, - types.GroupPermissionRedirectedSource: true, - }, permissions) - if _, err := client.Group.Create(). - SetName("User"). - SetStoragePoliciesID(1). - SetMaxStorage(1 * constants.GB). // 1 GB default storage - SetPermissions(permissions). - SetSettings(&types.GroupSetting{ - SourceBatchSize: 10, - Aria2BatchSize: 1, - MaxWalkedFiles: 100000, - TrashRetention: 7 * 24 * 3600, - RedirectedSource: true, - }). - Save(ctx); err != nil { - return fmt.Errorf("failed to create default user group: %w", err) - } - - return nil -} - -func migrateAnonymousGroup(l logging.Logger, client *ent.Client, ctx context.Context) error { - if _, err := client.Group.Query().Where(group.ID(AnonymousGroupID)).First(ctx); err == nil { - l.Info("Default anonymous group (ID=3) already exists, skip migrating.") - return nil - } - - l.Info("Insert default anonymous group...") - permissions := &boolset.BooleanSet{} - boolset.Sets(map[types.GroupPermission]bool{ - types.GroupPermissionIsAnonymous: true, - types.GroupPermissionShareDownload: true, - }, permissions) - if _, err := client.Group.Create(). - SetName("Anonymous"). - SetPermissions(permissions). - SetSettings(&types.GroupSetting{ - MaxWalkedFiles: 100000, - RedirectedSource: true, - }). - Save(ctx); err != nil { - return fmt.Errorf("failed to create default anonymous group: %w", err) - } - - return nil -} - -func migrateMasterNode(l logging.Logger, client *ent.Client, ctx context.Context) error { - if _, err := client.Node.Query().Where(node.TypeEQ(node.TypeMaster)).First(ctx); err == nil { - l.Info("Default master node already exists, skip migrating.") - return nil - } - - capabilities := &boolset.BooleanSet{} - boolset.Sets(map[types.NodeCapability]bool{ - types.NodeCapabilityCreateArchive: true, - types.NodeCapabilityExtractArchive: true, - types.NodeCapabilityRemoteDownload: true, - }, capabilities) - - stm := client.Node.Create(). - SetType(node.TypeMaster). - SetCapabilities(capabilities). - SetName("Master"). - SetSettings(&types.NodeSetting{ - Provider: types.DownloaderProviderAria2, - }). - SetStatus(node.StatusActive) - - _, enableAria2 := os.LookupEnv(EnvEnableAria2) - if enableAria2 { - l.Info("Aria2 is override as enabled.") - stm.SetSettings(&types.NodeSetting{ - Provider: types.DownloaderProviderAria2, - Aria2Setting: &types.Aria2Setting{ - Server: "http://127.0.0.1:6800/jsonrpc", - }, - }) - } - - l.Info("Insert default master node...") - if _, err := stm.Save(ctx); err != nil { - return fmt.Errorf("failed to create default master node: %w", err) - } - - return nil -} - -func createMockData(client *ent.Client, ctx context.Context) { - //userCount := 100 - //folderCount := 10000 - //fileCount := 25000 - // - //// create users - //pwdDigest, _ := digestPassword("52121225") - //userCreates := make([]*ent.UserCreate, userCount) - //for i := 0; i < userCount; i++ { - // nick := uuid.Must(uuid.NewV4()).String() - // userCreates[i] = client.User.Create(). - // SetEmail(nick + "@cloudreve.org"). - // SetNick(nick). - // SetPassword(pwdDigest). - // SetStatus(user.StatusActive). - // SetGroupID(1) - //} - //users, err := client.User.CreateBulk(userCreates...).Save(ctx) - //if err != nil { - // panic(err) - //} - // - //// Create root folder - //rootFolderCreates := make([]*ent.FileCreate, userCount) - //folderIds := make([][]int, 0, folderCount*userCount+userCount) - //for i, user := range users { - // rootFolderCreates[i] = client.File.Create(). - // SetName(RootFolderName). - // SetOwnerID(user.ID). - // SetType(int(FileTypeFolder)) - //} - //rootFolders, err := client.File.CreateBulk(rootFolderCreates...).Save(ctx) - //for _, rootFolders := range rootFolders { - // folderIds = append(folderIds, []int{rootFolders.ID, rootFolders.OwnerID}) - //} - //if err != nil { - // panic(err) - //} - // - //// create random folder - //for i := 0; i < folderCount*userCount; i++ { - // parent := lo.Sample(folderIds) - // res := client.File.Create(). - // SetName(uuid.Must(uuid.NewV4()).String()). - // SetType(int(FileTypeFolder)). - // SetOwnerID(parent[1]). - // SetFileChildren(parent[0]). - // SaveX(ctx) - // folderIds = append(folderIds, []int{res.ID, res.OwnerID}) - //} - - for i := 0; i < 255; i++ { - fmt.Printf("%d/", i) - } -} diff --git a/inventory/migration.go b/inventory/migration.go new file mode 100644 index 00000000..9f1e28b5 --- /dev/null +++ b/inventory/migration.go @@ -0,0 +1,416 @@ +package inventory + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/Masterminds/semver/v3" + "github.com/cloudreve/Cloudreve/v4/application/constants" + "github.com/cloudreve/Cloudreve/v4/ent" + "github.com/cloudreve/Cloudreve/v4/ent/group" + "github.com/cloudreve/Cloudreve/v4/ent/node" + "github.com/cloudreve/Cloudreve/v4/ent/setting" + "github.com/cloudreve/Cloudreve/v4/ent/storagepolicy" + "github.com/cloudreve/Cloudreve/v4/inventory/types" + "github.com/cloudreve/Cloudreve/v4/pkg/boolset" + "github.com/cloudreve/Cloudreve/v4/pkg/cache" + "github.com/cloudreve/Cloudreve/v4/pkg/logging" + "github.com/cloudreve/Cloudreve/v4/pkg/util" + "github.com/samber/lo" +) + +// needMigration exams if required schema version is satisfied. +func needMigration(client *ent.Client, ctx context.Context, requiredDbVersion string) bool { + c, _ := client.Setting.Query().Where(setting.NameEQ(DBVersionPrefix + requiredDbVersion)).Count(ctx) + return c == 0 +} + +func migrate(l logging.Logger, client *ent.Client, ctx context.Context, kv cache.Driver, requiredDbVersion string) error { + l.Info("Start initializing database schema...") + l.Info("Creating basic table schema...") + if err := client.Schema.Create(ctx); err != nil { + return fmt.Errorf("Failed creating schema resources: %w", err) + } + + migrateDefaultSettings(l, client, ctx, kv) + + if err := migrateDefaultStoragePolicy(l, client, ctx); err != nil { + return fmt.Errorf("failed migrating default storage policy: %w", err) + } + + if err := migrateSysGroups(l, client, ctx); err != nil { + return fmt.Errorf("failed migrating default storage policy: %w", err) + } + + if err := applyPatches(l, client, ctx, requiredDbVersion); err != nil { + return fmt.Errorf("failed applying schema patches: %w", err) + } + + client.Setting.Create().SetName(DBVersionPrefix + requiredDbVersion).SetValue("installed").Save(ctx) + return nil +} + +func migrateDefaultSettings(l logging.Logger, client *ent.Client, ctx context.Context, kv cache.Driver) { + // clean kv cache + if err := kv.DeleteAll(); err != nil { + l.Warning("Failed to remove all KV entries while schema migration: %s", err) + } + + // List existing settings into a map + existingSettings := make(map[string]struct{}) + settings, err := client.Setting.Query().All(ctx) + if err != nil { + l.Warning("Failed to query existing settings: %s", err) + } + + for _, s := range settings { + existingSettings[s.Name] = struct{}{} + } + + l.Info("Insert default settings...") + for k, v := range DefaultSettings { + if _, ok := existingSettings[k]; ok { + l.Debug("Skip inserting setting %s, already exists.", k) + continue + } + + if override, ok := os.LookupEnv(EnvDefaultOverwritePrefix + k); ok { + l.Info("Override default setting %q with env value %q", k, override) + v = override + } + + client.Setting.Create().SetName(k).SetValue(v).SaveX(ctx) + } +} + +func migrateDefaultStoragePolicy(l logging.Logger, client *ent.Client, ctx context.Context) error { + if _, err := client.StoragePolicy.Query().Where(storagepolicy.ID(1)).First(ctx); err == nil { + l.Info("Default storage policy (ID=1) already exists, skip migrating.") + return nil + } + + l.Info("Insert default storage policy...") + if _, err := client.StoragePolicy.Create(). + SetName("Default storage policy"). + SetType(types.PolicyTypeLocal). + SetDirNameRule(util.DataPath("uploads/{uid}/{path}")). + SetFileNameRule("{uid}_{randomkey8}_{originname}"). + SetSettings(&types.PolicySetting{ + ChunkSize: 25 << 20, // 25MB + PreAllocate: true, + }). + Save(ctx); err != nil { + return fmt.Errorf("failed to create default storage policy: %w", err) + } + + return nil +} + +func migrateSysGroups(l logging.Logger, client *ent.Client, ctx context.Context) error { + if err := migrateAdminGroup(l, client, ctx); err != nil { + return err + } + + if err := migrateUserGroup(l, client, ctx); err != nil { + return err + } + + if err := migrateAnonymousGroup(l, client, ctx); err != nil { + return err + } + + if err := migrateMasterNode(l, client, ctx); err != nil { + return err + } + + return nil +} + +func migrateAdminGroup(l logging.Logger, client *ent.Client, ctx context.Context) error { + if _, err := client.Group.Query().Where(group.ID(1)).First(ctx); err == nil { + l.Info("Default admin group (ID=1) already exists, skip migrating.") + return nil + } + + l.Info("Insert default admin group...") + permissions := &boolset.BooleanSet{} + boolset.Sets(map[types.GroupPermission]bool{ + types.GroupPermissionIsAdmin: true, + types.GroupPermissionShare: true, + types.GroupPermissionWebDAV: true, + types.GroupPermissionWebDAVProxy: true, + types.GroupPermissionArchiveDownload: true, + types.GroupPermissionArchiveTask: true, + types.GroupPermissionShareDownload: true, + types.GroupPermissionRemoteDownload: true, + types.GroupPermissionRedirectedSource: true, + types.GroupPermissionAdvanceDelete: true, + types.GroupPermissionIgnoreFileOwnership: true, + // TODO: review default permission + }, permissions) + if _, err := client.Group.Create(). + SetName("Admin"). + SetStoragePoliciesID(1). + SetMaxStorage(1 * constants.TB). // 1 TB default storage + SetPermissions(permissions). + SetSettings(&types.GroupSetting{ + SourceBatchSize: 1000, + Aria2BatchSize: 50, + MaxWalkedFiles: 100000, + TrashRetention: 7 * 24 * 3600, + RedirectedSource: true, + }). + Save(ctx); err != nil { + return fmt.Errorf("failed to create default admin group: %w", err) + } + + return nil +} + +func migrateUserGroup(l logging.Logger, client *ent.Client, ctx context.Context) error { + if _, err := client.Group.Query().Where(group.ID(2)).First(ctx); err == nil { + l.Info("Default user group (ID=2) already exists, skip migrating.") + return nil + } + + l.Info("Insert default user group...") + permissions := &boolset.BooleanSet{} + boolset.Sets(map[types.GroupPermission]bool{ + types.GroupPermissionShare: true, + types.GroupPermissionShareDownload: true, + types.GroupPermissionRedirectedSource: true, + }, permissions) + if _, err := client.Group.Create(). + SetName("User"). + SetStoragePoliciesID(1). + SetMaxStorage(1 * constants.GB). // 1 GB default storage + SetPermissions(permissions). + SetSettings(&types.GroupSetting{ + SourceBatchSize: 10, + Aria2BatchSize: 1, + MaxWalkedFiles: 100000, + TrashRetention: 7 * 24 * 3600, + RedirectedSource: true, + }). + Save(ctx); err != nil { + return fmt.Errorf("failed to create default user group: %w", err) + } + + return nil +} + +func migrateAnonymousGroup(l logging.Logger, client *ent.Client, ctx context.Context) error { + if _, err := client.Group.Query().Where(group.ID(AnonymousGroupID)).First(ctx); err == nil { + l.Info("Default anonymous group (ID=3) already exists, skip migrating.") + return nil + } + + l.Info("Insert default anonymous group...") + permissions := &boolset.BooleanSet{} + boolset.Sets(map[types.GroupPermission]bool{ + types.GroupPermissionIsAnonymous: true, + types.GroupPermissionShareDownload: true, + }, permissions) + if _, err := client.Group.Create(). + SetName("Anonymous"). + SetPermissions(permissions). + SetSettings(&types.GroupSetting{ + MaxWalkedFiles: 100000, + RedirectedSource: true, + }). + Save(ctx); err != nil { + return fmt.Errorf("failed to create default anonymous group: %w", err) + } + + return nil +} + +func migrateMasterNode(l logging.Logger, client *ent.Client, ctx context.Context) error { + if _, err := client.Node.Query().Where(node.TypeEQ(node.TypeMaster)).First(ctx); err == nil { + l.Info("Default master node already exists, skip migrating.") + return nil + } + + capabilities := &boolset.BooleanSet{} + boolset.Sets(map[types.NodeCapability]bool{ + types.NodeCapabilityCreateArchive: true, + types.NodeCapabilityExtractArchive: true, + types.NodeCapabilityRemoteDownload: true, + }, capabilities) + + stm := client.Node.Create(). + SetType(node.TypeMaster). + SetCapabilities(capabilities). + SetName("Master"). + SetSettings(&types.NodeSetting{ + Provider: types.DownloaderProviderAria2, + }). + SetStatus(node.StatusActive) + + _, enableAria2 := os.LookupEnv(EnvEnableAria2) + if enableAria2 { + l.Info("Aria2 is override as enabled.") + stm.SetSettings(&types.NodeSetting{ + Provider: types.DownloaderProviderAria2, + Aria2Setting: &types.Aria2Setting{ + Server: "http://127.0.0.1:6800/jsonrpc", + }, + }) + } + + l.Info("Insert default master node...") + if _, err := stm.Save(ctx); err != nil { + return fmt.Errorf("failed to create default master node: %w", err) + } + + return nil +} + +type ( + PatchFunc func(l logging.Logger, client *ent.Client, ctx context.Context) error + Patch struct { + Name string + EndVersion string + Func PatchFunc + } +) + +var patches = []Patch{ + { + Name: "apply_default_excalidraw_viewer", + EndVersion: "4.1.0", + Func: func(l logging.Logger, client *ent.Client, ctx context.Context) error { + // 1. Apply excalidraw file icons + // 1.1 Check if it's already applied + iconSetting, err := client.Setting.Query().Where(setting.Name("explorer_icons")).First(ctx) + if err != nil { + return fmt.Errorf("failed to query explorer_icons setting: %w", err) + } + + var icons []types.FileTypeIconSetting + if err := json.Unmarshal([]byte(iconSetting.Value), &icons); err != nil { + return fmt.Errorf("failed to unmarshal explorer_icons setting: %w", err) + } + + iconExisted := false + for _, icon := range icons { + if lo.Contains(icon.Exts, "excalidraw") { + iconExisted = true + break + } + } + + // 1.2 If not existed, add it + if !iconExisted { + // Found existing excalidraw icon default setting + var defaultExcalidrawIcon types.FileTypeIconSetting + for _, icon := range defaultIcons { + if lo.Contains(icon.Exts, "excalidraw") { + defaultExcalidrawIcon = icon + break + } + } + + icons = append(icons, defaultExcalidrawIcon) + newIconSetting, err := json.Marshal(icons) + if err != nil { + return fmt.Errorf("failed to marshal explorer_icons setting: %w", err) + } + + if _, err := client.Setting.UpdateOne(iconSetting).SetValue(string(newIconSetting)).Save(ctx); err != nil { + return fmt.Errorf("failed to update explorer_icons setting: %w", err) + } + } + + // 2. Apply default file viewers + // 2.1 Check if it's already applied + fileViewersSetting, err := client.Setting.Query().Where(setting.Name("file_viewers")).First(ctx) + if err != nil { + return fmt.Errorf("failed to query file_viewers setting: %w", err) + } + + var fileViewers []types.ViewerGroup + if err := json.Unmarshal([]byte(fileViewersSetting.Value), &fileViewers); err != nil { + return fmt.Errorf("failed to unmarshal file_viewers setting: %w", err) + } + + fileViewerExisted := false + for _, viewer := range fileViewers[0].Viewers { + if viewer.ID == "excalidraw" { + fileViewerExisted = true + break + } + } + + // 2.2 If not existed, add it + if !fileViewerExisted { + // Found existing excalidraw viewer default setting + var defaultExcalidrawViewer types.Viewer + for _, viewer := range defaultFileViewers[0].Viewers { + if viewer.ID == "excalidraw" { + defaultExcalidrawViewer = viewer + break + } + } + + fileViewers[0].Viewers = append(fileViewers[0].Viewers, defaultExcalidrawViewer) + newFileViewersSetting, err := json.Marshal(fileViewers) + if err != nil { + return fmt.Errorf("failed to marshal file_viewers setting: %w", err) + } + + if _, err := client.Setting.UpdateOne(fileViewersSetting).SetValue(string(newFileViewersSetting)).Save(ctx); err != nil { + return fmt.Errorf("failed to update file_viewers setting: %w", err) + } + } + + return nil + }, + }, +} + +func applyPatches(l logging.Logger, client *ent.Client, ctx context.Context, requiredDbVersion string) error { + allVersionMarks, err := client.Setting.Query().Where(setting.NameHasPrefix(DBVersionPrefix)).All(ctx) + if err != nil { + return err + } + + requiredDbVersion = strings.TrimSuffix(requiredDbVersion, "-pro") + + // Find the latest applied version + var latestAppliedVersion *semver.Version + for _, v := range allVersionMarks { + v.Name = strings.TrimSuffix(v.Name, "-pro") + version, err := semver.NewVersion(strings.TrimPrefix(v.Name, DBVersionPrefix)) + if err != nil { + l.Warning("Failed to parse past version %s: %s", v.Name, err) + continue + } + if latestAppliedVersion == nil || version.Compare(latestAppliedVersion) > 0 { + latestAppliedVersion = version + } + } + + requiredVersion, err := semver.NewVersion(requiredDbVersion) + if err != nil { + return fmt.Errorf("failed to parse required version %s: %w", requiredDbVersion, err) + } + + if latestAppliedVersion == nil || requiredVersion.Compare(requiredVersion) > 0 { + latestAppliedVersion = requiredVersion + } + + for _, patch := range patches { + if latestAppliedVersion.Compare(semver.MustParse(patch.EndVersion)) < 0 { + l.Info("Applying schema patch %s...", patch.Name) + if err := patch.Func(l, client, ctx); err != nil { + return err + } + } + } + + return nil +} diff --git a/inventory/setting.go b/inventory/setting.go index cae67151..45b5e8c4 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -2,10 +2,12 @@ package inventory import ( "context" + "encoding/json" "fmt" "github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/ent/setting" + "github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/pkg/cache" "github.com/cloudreve/Cloudreve/v4/pkg/util" "github.com/gofrs/uuid" @@ -77,6 +79,253 @@ func (c *settingClient) Set(ctx context.Context, settings map[string]string) err return nil } +var ( + defaultIcons = []types.FileTypeIconSetting{ + { + Exts: []string{"mp3", "flac", "ape", "wav", "acc", "ogg", "m4a"}, + Icon: "audio", + Color: "#651fff", + }, + { + Exts: []string{"m3u8", "mp4", "flv", "avi", "wmv", "mkv", "rm", "rmvb", "mov", "ogv"}, + Icon: "video", + Color: "#d50000", + }, + { + Exts: []string{"bmp", "iff", "png", "gif", "jpg", "jpeg", "psd", "svg", "webp", "heif", "heic", "tiff", "avif"}, + Icon: "image", + Color: "#d32f2f", + }, + { + Exts: []string{"3fr", "ari", "arw", "bay", "braw", "crw", "cr2", "cr3", "cap", "dcs", "dcr", "dng", "drf", "eip", "erf", "fff", "gpr", "iiq", "k25", "kdc", "mdc", "mef", "mos", "mrw", "nef", "nrw", "obm", "orf", "pef", "ptx", "pxn", "r3d", "raf", "raw", "rwl", "rw2", "rwz", "sr2", "srf", "srw", "tif", "x3f"}, + Icon: "raw", + Color: "#d32f2f", + }, + { + Exts: []string{"pdf"}, + Color: "#f44336", + Icon: "pdf", + }, + { + Exts: []string{"doc", "docx"}, + Color: "#538ce5", + Icon: "word", + }, + { + Exts: []string{"ppt", "pptx"}, + Color: "#EF633F", + Icon: "ppt", + }, + { + Exts: []string{"xls", "xlsx", "csv"}, + Color: "#4caf50", + Icon: "excel", + }, + { + Exts: []string{"txt", "html"}, + Color: "#607d8b", + Icon: "text", + }, + { + Exts: []string{"torrent"}, + Color: "#5c6bc0", + Icon: "torrent", + }, + { + Exts: []string{"zip", "gz", "xz", "tar", "rar", "7z", "bz2", "z"}, + Color: "#f9a825", + Icon: "zip", + }, + { + Exts: []string{"exe", "msi"}, + Color: "#1a237e", + Icon: "exe", + }, + { + Exts: []string{"apk"}, + Color: "#8bc34a", + Icon: "android", + }, + { + Exts: []string{"go"}, + Color: "#16b3da", + Icon: "go", + }, + { + Exts: []string{"py"}, + Color: "#3776ab", + Icon: "python", + }, + { + Exts: []string{"c"}, + Color: "#a4c639", + Icon: "c", + }, + { + Exts: []string{"cpp"}, + Color: "#f34b7d", + Icon: "cpp", + }, + { + Exts: []string{"js", "jsx"}, + Color: "#f4d003", + Icon: "js", + }, + { + Exts: []string{"epub"}, + Color: "#81b315", + Icon: "book", + }, + { + Exts: []string{"rs"}, + Color: "#000", + ColorDark: "#fff", + Icon: "rust", + }, + { + Exts: []string{"drawio"}, + Color: "#F08705", + Icon: "flowchart", + }, + { + Exts: []string{"dwb"}, + Color: "#F08705", + Icon: "whiteboard", + }, + { + Exts: []string{"md"}, + Color: "#383838", + ColorDark: "#cbcbcb", + Icon: "markdown", + }, + { + Img: "/static/img/viewers/excalidraw.svg", + Exts: []string{"excalidraw"}, + }, + } + + defaultFileViewers = []types.ViewerGroup{ + { + Viewers: []types.Viewer{ + { + ID: "music", + Type: types.ViewerTypeBuiltin, + DisplayName: "fileManager.musicPlayer", + Exts: []string{"mp3", "ogg", "wav", "flac", "m4a"}, + }, + { + ID: "epub", + Type: types.ViewerTypeBuiltin, + DisplayName: "fileManager.epubViewer", + Exts: []string{"epub"}, + }, + { + ID: "googledocs", + Type: types.ViewerTypeCustom, + DisplayName: "fileManager.googledocs", + Icon: "/static/img/viewers/gdrive.png", + Url: "https://docs.google.com/gview?url={$src}&embedded=true", + Exts: []string{"jpeg", "png", "gif", "tiff", "bmp", "webm", "mpeg4", "3gpp", "mov", "avi", "mpegps", "wmv", "flv", "txt", "css", "html", "php", "c", "cpp", "h", "hpp", "js", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "pdf", "pages", "ai", "psd", "tiff", "dxf", "svg", "eps", "ps", "ttf", "xps"}, + MaxSize: 26214400, + }, + { + ID: "m365online", + Type: types.ViewerTypeCustom, + DisplayName: "fileManager.m365viewer", + Icon: "/static/img/viewers/m365.svg", + Url: "https://view.officeapps.live.com/op/view.aspx?src={$src}", + Exts: []string{"doc", "docx", "docm", "dotm", "dotx", "xlsx", "xlsb", "xls", "xlsm", "pptx", "ppsx", "ppt", "pps", "pptm", "potm", "ppam", "potx", "ppsm"}, + MaxSize: 10485760, + }, + { + ID: "pdf", + Type: types.ViewerTypeBuiltin, + DisplayName: "fileManager.pdfViewer", + Exts: []string{"pdf"}, + }, + { + ID: "video", + Type: types.ViewerTypeBuiltin, + Icon: "/static/img/viewers/artplayer.png", + DisplayName: "Artplayer", + Exts: []string{"mp4", "mkv", "webm", "avi", "mov", "m3u8", "flv"}, + }, + { + ID: "markdown", + Type: types.ViewerTypeBuiltin, + DisplayName: "fileManager.markdownEditor", + Exts: []string{"md"}, + Templates: []types.NewFileTemplate{ + { + Ext: "md", + DisplayName: "Markdown", + }, + }, + }, + { + ID: "drawio", + Type: types.ViewerTypeBuiltin, + Icon: "/static/img/viewers/drawio.svg", + DisplayName: "draw.io", + Exts: []string{"drawio", "dwb"}, + Props: map[string]string{ + "host": "https://embed.diagrams.net", + }, + Templates: []types.NewFileTemplate{ + { + Ext: "drawio", + DisplayName: "fileManager.diagram", + }, + { + Ext: "dwb", + DisplayName: "fileManager.whiteboard", + }, + }, + }, + { + ID: "image", + Type: types.ViewerTypeBuiltin, + DisplayName: "fileManager.imageViewer", + Exts: []string{"bmp", "png", "gif", "jpg", "jpeg", "svg", "webp", "heic", "heif"}, + }, + { + ID: "monaco", + Type: types.ViewerTypeBuiltin, + Icon: "/static/img/viewers/monaco.svg", + DisplayName: "fileManager.monacoEditor", + Exts: []string{"md", "txt", "json", "php", "py", "bat", "c", "h", "cpp", "hpp", "cs", "css", "dockerfile", "go", "html", "htm", "ini", "java", "js", "jsx", "less", "lua", "sh", "sql", "xml", "yaml"}, + Templates: []types.NewFileTemplate{ + { + Ext: "txt", + DisplayName: "fileManager.text", + }, + }, + }, + { + ID: "photopea", + Type: types.ViewerTypeBuiltin, + Icon: "/static/img/viewers/photopea.png", + DisplayName: "Photopea", + Exts: []string{"psd", "ai", "indd", "xcf", "xd", "fig", "kri", "clip", "pxd", "pxz", "cdr", "ufo", "afphoyo", "svg", "esp", "pdf", "pdn", "wmf", "emf", "png", "jpg", "jpeg", "gif", "webp", "ico", "icns", "bmp", "avif", "heic", "jxl", "ppm", "pgm", "pbm", "tiff", "dds", "iff", "anim", "tga", "dng", "nef", "cr2", "cr3", "arw", "rw2", "raf", "orf", "gpr", "3fr", "fff"}, + }, + { + ID: "excalidraw", + Type: types.ViewerTypeBuiltin, + Icon: "/static/img/viewers/excalidraw.svg", + DisplayName: "Excalidraw", + Exts: []string{"excalidraw"}, + Templates: []types.NewFileTemplate{ + { + Ext: "excalidraw", + DisplayName: "Excalidraw", + }, + }, + }, + }, + }, + } +) + var DefaultSettings = map[string]string{ "siteURL": `http://localhost:5212`, "siteName": `Cloudreve`, @@ -233,7 +482,6 @@ var DefaultSettings = map[string]string{ "site_logo_light": "/static/img/logo_light.svg", "tos_url": "https://cloudreve.org/privacy-policy", "privacy_policy_url": "https://cloudreve.org/privacy-policy", - "explorer_icons": `[{"exts":["mp3","flac","ape","wav","acc","ogg","m4a"],"icon":"audio","color":"#651fff"},{"exts":["m3u8","mp4","flv","avi","wmv","mkv","rm","rmvb","mov","ogv"],"icon":"video","color":"#d50000"},{"exts":["bmp","iff","png","gif","jpg","jpeg","psd","svg","webp","heif","heic","tiff","avif"],"icon":"image","color":"#d32f2f"},{"exts":["3fr","ari","arw","bay","braw","crw","cr2","cr3","cap","dcs","dcr","dng","drf","eip","erf","fff","gpr","iiq","k25","kdc","mdc","mef","mos","mrw","nef","nrw","obm","orf","pef","ptx","pxn","r3d","raf","raw","rwl","rw2","rwz","sr2","srf","srw","tif","x3f"],"icon":"raw","color":"#d32f2f"},{"exts":["pdf"],"color":"#f44336","icon":"pdf"},{"exts":["doc","docx"],"color":"#538ce5","icon":"word"},{"exts":["ppt","pptx"],"color":"#EF633F","icon":"ppt"},{"exts":["xls","xlsx","csv"],"color":"#4caf50","icon":"excel"},{"exts":["txt","html"],"color":"#607d8b","icon":"text"},{"exts":["torrent"],"color":"#5c6bc0","icon":"torrent"},{"exts":["zip","gz","xz","tar","rar","7z","bz2","z"],"color":"#f9a825","icon":"zip"},{"exts":["exe","msi"],"color":"#1a237e","icon":"exe"},{"exts":["apk"],"color":"#8bc34a","icon":"android"},{"exts":["go"],"color":"#16b3da","icon":"go"},{"exts":["py"],"color":"#3776ab","icon":"python"},{"exts":["c"],"color":"#a4c639","icon":"c"},{"exts":["cpp"],"color":"#f34b7d","icon":"cpp"},{"exts":["js","jsx"],"color":"#f4d003","icon":"js"},{"exts":["epub"],"color":"#81b315","icon":"book"},{"exts":["rs"],"color":"#000","color_dark":"#fff","icon":"rust"},{"exts":["drawio"],"color":"#F08705","icon":"flowchart"},{"exts":["dwb"],"color":"#F08705","icon":"whiteboard"},{"exts":["md"],"color":"#383838","color_dark":"#cbcbcb","icon":"markdown"},{"img":"/static/img/viewers/excalidraw.svg","exts":["excalidraw"]}]`, "explorer_category_image_query": "type=file&case_folding&use_or&name=*.bmp&name=*.iff&name=*.png&name=*.gif&name=*.jpg&name=*.jpeg&name=*.psd&name=*.svg&name=*.webp&name=*.heif&name=*.heic&name=*.tiff&name=*.avif&name=*.3fr&name=*.ari&name=*.arw&name=*.bay&name=*.braw&name=*.crw&name=*.cr2&name=*.cr3&name=*.cap&name=*.dcs&name=*.dcr&name=*.dng&name=*.drf&name=*.eip&name=*.erf&name=*.fff&name=*.gpr&name=*.iiq&name=*.k25&name=*.kdc&name=*.mdc&name=*.mef&name=*.mos&name=*.mrw&name=*.nef&name=*.nrw&name=*.obm&name=*.orf&name=*.pef&name=*.ptx&name=*.pxn&name=*.r3d&name=*.raf&name=*.raw&name=*.rwl&name=*.rw2&name=*.rwz&name=*.sr2&name=*.srf&name=*.srw&name=*.tif&name=*.x3f", "explorer_category_video_query": "type=file&case_folding&use_or&name=*.mp4&name=*.m3u8&name=*.flv&name=*.avi&name=*.wmv&name=*.mkv&name=*.rm&name=*.rmvb&name=*.mov&name=*.ogv", "explorer_category_audio_query": "type=file&case_folding&use_or&name=*.mp3&name=*.flac&name=*.ape&name=*.wav&name=*.acc&name=*.ogg&name=*.m4a", @@ -243,10 +491,24 @@ var DefaultSettings = map[string]string{ "map_provider": "openstreetmap", "map_google_tile_type": "regular", "mime_mapping": `{".xlsx":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",".xltx":"application/vnd.openxmlformats-officedocument.spreadsheetml.template",".potx":"application/vnd.openxmlformats-officedocument.presentationml.template",".ppsx":"application/vnd.openxmlformats-officedocument.presentationml.slideshow",".pptx":"application/vnd.openxmlformats-officedocument.presentationml.presentation",".sldx":"application/vnd.openxmlformats-officedocument.presentationml.slide",".docx":"application/vnd.openxmlformats-officedocument.wordprocessingml.document",".dotx":"application/vnd.openxmlformats-officedocument.wordprocessingml.template",".xlam":"application/vnd.ms-excel.addin.macroEnabled.12",".xlsb":"application/vnd.ms-excel.sheet.binary.macroEnabled.12",".apk":"application/vnd.android.package-archive",".hqx":"application/mac-binhex40",".cpt":"application/mac-compactpro",".doc":"application/msword",".ogg":"application/ogg",".pdf":"application/pdf",".rtf":"text/rtf",".mif":"application/vnd.mif",".xls":"application/vnd.ms-excel",".ppt":"application/vnd.ms-powerpoint",".odc":"application/vnd.oasis.opendocument.chart",".odb":"application/vnd.oasis.opendocument.database",".odf":"application/vnd.oasis.opendocument.formula",".odg":"application/vnd.oasis.opendocument.graphics",".otg":"application/vnd.oasis.opendocument.graphics-template",".odi":"application/vnd.oasis.opendocument.image",".odp":"application/vnd.oasis.opendocument.presentation",".otp":"application/vnd.oasis.opendocument.presentation-template",".ods":"application/vnd.oasis.opendocument.spreadsheet",".ots":"application/vnd.oasis.opendocument.spreadsheet-template",".odt":"application/vnd.oasis.opendocument.text",".odm":"application/vnd.oasis.opendocument.text-master",".ott":"application/vnd.oasis.opendocument.text-template",".oth":"application/vnd.oasis.opendocument.text-web",".sxw":"application/vnd.sun.xml.writer",".stw":"application/vnd.sun.xml.writer.template",".sxc":"application/vnd.sun.xml.calc",".stc":"application/vnd.sun.xml.calc.template",".sxd":"application/vnd.sun.xml.draw",".std":"application/vnd.sun.xml.draw.template",".sxi":"application/vnd.sun.xml.impress",".sti":"application/vnd.sun.xml.impress.template",".sxg":"application/vnd.sun.xml.writer.global",".sxm":"application/vnd.sun.xml.math",".sis":"application/vnd.symbian.install",".wbxml":"application/vnd.wap.wbxml",".wmlc":"application/vnd.wap.wmlc",".wmlsc":"application/vnd.wap.wmlscriptc",".bcpio":"application/x-bcpio",".torrent":"application/x-bittorrent",".bz2":"application/x-bzip2",".vcd":"application/x-cdlink",".pgn":"application/x-chess-pgn",".cpio":"application/x-cpio",".csh":"application/x-csh",".dvi":"application/x-dvi",".spl":"application/x-futuresplash",".gtar":"application/x-gtar",".hdf":"application/x-hdf",".jar":"application/x-java-archive",".jnlp":"application/x-java-jnlp-file",".js":"application/x-javascript",".ksp":"application/x-kspread",".chrt":"application/x-kchart",".kil":"application/x-killustrator",".latex":"application/x-latex",".rpm":"application/x-rpm",".sh":"application/x-sh",".shar":"application/x-shar",".swf":"application/x-shockwave-flash",".sit":"application/x-stuffit",".sv4cpio":"application/x-sv4cpio",".sv4crc":"application/x-sv4crc",".tar":"application/x-tar",".tcl":"application/x-tcl",".tex":"application/x-tex",".man":"application/x-troff-man",".me":"application/x-troff-me",".ms":"application/x-troff-ms",".ustar":"application/x-ustar",".src":"application/x-wais-source",".zip":"application/zip",".m3u":"audio/x-mpegurl",".ra":"audio/x-pn-realaudio",".wav":"audio/x-wav",".wma":"audio/x-ms-wma",".wax":"audio/x-ms-wax",".pdb":"chemical/x-pdb",".xyz":"chemical/x-xyz",".bmp":"image/bmp",".gif":"image/gif",".ief":"image/ief",".png":"image/png",".wbmp":"image/vnd.wap.wbmp",".ras":"image/x-cmu-raster",".pnm":"image/x-portable-anymap",".pbm":"image/x-portable-bitmap",".pgm":"image/x-portable-graymap",".ppm":"image/x-portable-pixmap",".rgb":"image/x-rgb",".xbm":"image/x-xbitmap",".xpm":"image/x-xpixmap",".xwd":"image/x-xwindowdump",".css":"text/css",".rtx":"text/richtext",".tsv":"text/tab-separated-values",".jad":"text/vnd.sun.j2me.app-descriptor",".wml":"text/vnd.wap.wml",".wmls":"text/vnd.wap.wmlscript",".etx":"text/x-setext",".mxu":"video/vnd.mpegurl",".flv":"video/x-flv",".wm":"video/x-ms-wm",".wmv":"video/x-ms-wmv",".wmx":"video/x-ms-wmx",".wvx":"video/x-ms-wvx",".avi":"video/x-msvideo",".movie":"video/x-sgi-movie",".ice":"x-conference/x-cooltalk",".3gp":"video/3gpp",".ai":"application/postscript",".aif":"audio/x-aiff",".aifc":"audio/x-aiff",".aiff":"audio/x-aiff",".asc":"text/plain",".atom":"application/atom+xml",".au":"audio/basic",".bin":"application/octet-stream",".cdf":"application/x-netcdf",".cgm":"image/cgm",".class":"application/octet-stream",".dcr":"application/x-director",".dif":"video/x-dv",".dir":"application/x-director",".djv":"image/vnd.djvu",".djvu":"image/vnd.djvu",".dll":"application/octet-stream",".dmg":"application/octet-stream",".dms":"application/octet-stream",".dtd":"application/xml-dtd",".dv":"video/x-dv",".dxr":"application/x-director",".eps":"application/postscript",".exe":"application/octet-stream",".ez":"application/andrew-inset",".gram":"application/srgs",".grxml":"application/srgs+xml",".gz":"application/x-gzip",".htm":"text/html",".html":"text/html",".ico":"image/x-icon",".ics":"text/calendar",".ifb":"text/calendar",".iges":"model/iges",".igs":"model/iges",".jp2":"image/jp2",".jpe":"image/jpeg",".jpeg":"image/jpeg",".jpg":"image/jpeg",".kar":"audio/midi",".lha":"application/octet-stream",".lzh":"application/octet-stream",".m4a":"audio/mp4a-latm",".m4p":"audio/mp4a-latm",".m4u":"video/vnd.mpegurl",".m4v":"video/x-m4v",".mac":"image/x-macpaint",".mathml":"application/mathml+xml",".mesh":"model/mesh",".mid":"audio/midi",".midi":"audio/midi",".mov":"video/quicktime",".mp2":"audio/mpeg",".mp3":"audio/mpeg",".mp4":"video/mp4",".mpe":"video/mpeg",".mpeg":"video/mpeg",".mpg":"video/mpeg",".mpga":"audio/mpeg",".msh":"model/mesh",".nc":"application/x-netcdf",".oda":"application/oda",".ogv":"video/ogv",".pct":"image/pict",".pic":"image/pict",".pict":"image/pict",".pnt":"image/x-macpaint",".pntg":"image/x-macpaint",".ps":"application/postscript",".qt":"video/quicktime",".qti":"image/x-quicktime",".qtif":"image/x-quicktime",".ram":"audio/x-pn-realaudio",".rdf":"application/rdf+xml",".rm":"application/vnd.rn-realmedia",".roff":"application/x-troff",".sgm":"text/sgml",".sgml":"text/sgml",".silo":"model/mesh",".skd":"application/x-koan",".skm":"application/x-koan",".skp":"application/x-koan",".skt":"application/x-koan",".smi":"application/smil",".smil":"application/smil",".snd":"audio/basic",".so":"application/octet-stream",".svg":"image/svg+xml",".t":"application/x-troff",".texi":"application/x-texinfo",".texinfo":"application/x-texinfo",".tif":"image/tiff",".tiff":"image/tiff",".tr":"application/x-troff",".txt":"text/plain; charset=utf-8",".vrml":"model/vrml",".vxml":"application/voicexml+xml",".webm":"video/webm",".wrl":"model/vrml",".xht":"application/xhtml+xml",".xhtml":"application/xhtml+xml",".xml":"application/xml",".xsl":"application/xml",".xslt":"application/xslt+xml",".xul":"application/vnd.mozilla.xul+xml",".webp":"image/webp",".323":"text/h323",".aab":"application/x-authoware-bin",".aam":"application/x-authoware-map",".aas":"application/x-authoware-seg",".acx":"application/internet-property-stream",".als":"audio/X-Alpha5",".amc":"application/x-mpeg",".ani":"application/octet-stream",".asd":"application/astound",".asf":"video/x-ms-asf",".asn":"application/astound",".asp":"application/x-asap",".asr":"video/x-ms-asf",".asx":"video/x-ms-asf",".avb":"application/octet-stream",".awb":"audio/amr-wb",".axs":"application/olescript",".bas":"text/plain",".bin ":"application/octet-stream",".bld":"application/bld",".bld2":"application/bld2",".bpk":"application/octet-stream",".c":"text/plain",".cal":"image/x-cals",".cat":"application/vnd.ms-pkiseccat",".ccn":"application/x-cnc",".cco":"application/x-cocoa",".cer":"application/x-x509-ca-cert",".cgi":"magnus-internal/cgi",".chat":"application/x-chat",".clp":"application/x-msclip",".cmx":"image/x-cmx",".co":"application/x-cult3d-object",".cod":"image/cis-cod",".conf":"text/plain",".cpp":"text/plain",".crd":"application/x-mscardfile",".crl":"application/pkix-crl",".crt":"application/x-x509-ca-cert",".csm":"chemical/x-csml",".csml":"chemical/x-csml",".cur":"application/octet-stream",".dcm":"x-lml/x-evm",".dcx":"image/x-dcx",".der":"application/x-x509-ca-cert",".dhtml":"text/html",".dot":"application/msword",".dwf":"drawing/x-dwf",".dwg":"application/x-autocad",".dxf":"application/x-autocad",".ebk":"application/x-expandedbook",".emb":"chemical/x-embl-dl-nucleotide",".embl":"chemical/x-embl-dl-nucleotide",".epub":"application/epub+zip",".eri":"image/x-eri",".es":"audio/echospeech",".esl":"audio/echospeech",".etc":"application/x-earthtime",".evm":"x-lml/x-evm",".evy":"application/envoy",".fh4":"image/x-freehand",".fh5":"image/x-freehand",".fhc":"image/x-freehand",".fif":"application/fractals",".flr":"x-world/x-vrml",".fm":"application/x-maker",".fpx":"image/x-fpx",".fvi":"video/isivideo",".gau":"chemical/x-gaussian-input",".gca":"application/x-gca-compressed",".gdb":"x-lml/x-gdb",".gps":"application/x-gps",".h":"text/plain",".hdm":"text/x-hdml",".hdml":"text/x-hdml",".hlp":"application/winhlp",".hta":"application/hta",".htc":"text/x-component",".hts":"text/html",".htt":"text/webviewhtml",".ifm":"image/gif",".ifs":"image/ifs",".iii":"application/x-iphone",".imy":"audio/melody",".ins":"application/x-internet-signup",".ips":"application/x-ipscript",".ipx":"application/x-ipix",".isp":"application/x-internet-signup",".it":"audio/x-mod",".itz":"audio/x-mod",".ivr":"i-world/i-vrml",".j2k":"image/j2k",".jam":"application/x-jam",".java":"text/plain",".jfif":"image/pipeg",".jpz":"image/jpeg",".jwc":"application/jwc",".kjx":"application/x-kjx",".lak":"x-lml/x-lak",".lcc":"application/fastman",".lcl":"application/x-digitalloca",".lcr":"application/x-digitalloca",".lgh":"application/lgh",".lml":"x-lml/x-lml",".lmlpack":"x-lml/x-lmlpack",".log":"text/plain",".lsf":"video/x-la-asf",".lsx":"video/x-la-asf",".m13":"application/x-msmediaview",".m14":"application/x-msmediaview",".m15":"audio/x-mod",".m3url":"audio/x-mpegurl",".m4b":"audio/mp4a-latm",".ma1":"audio/ma1",".ma2":"audio/ma2",".ma3":"audio/ma3",".ma5":"audio/ma5",".map":"magnus-internal/imagemap",".mbd":"application/mbedlet",".mct":"application/x-mascot",".mdb":"application/x-msaccess",".mdz":"audio/x-mod",".mel":"text/x-vmel",".mht":"message/rfc822",".mhtml":"message/rfc822",".mi":"application/x-mif",".mil":"image/x-cals",".mio":"audio/x-mio",".mmf":"application/x-skt-lbs",".mng":"video/x-mng",".mny":"application/x-msmoney",".moc":"application/x-mocha",".mocha":"application/x-mocha",".mod":"audio/x-mod",".mof":"application/x-yumekara",".mol":"chemical/x-mdl-molfile",".mop":"chemical/x-mopac-input",".mpa":"video/mpeg",".mpc":"application/vnd.mpohun.certificate",".mpg4":"video/mp4",".mpn":"application/vnd.mophun.application",".mpp":"application/vnd.ms-project",".mps":"application/x-mapserver",".mpv2":"video/mpeg",".mrl":"text/x-mrml",".mrm":"application/x-mrm",".msg":"application/vnd.ms-outlook",".mts":"application/metastream",".mtx":"application/metastream",".mtz":"application/metastream",".mvb":"application/x-msmediaview",".mzv":"application/metastream",".nar":"application/zip",".nbmp":"image/nbmp",".ndb":"x-lml/x-ndb",".ndwn":"application/ndwn",".nif":"application/x-nif",".nmz":"application/x-scream",".nokia-op-logo":"image/vnd.nok-oplogo-color",".npx":"application/x-netfpx",".nsnd":"audio/nsnd",".nva":"application/x-neva1",".nws":"message/rfc822",".oom":"application/x-AtlasMate-Plugin",".p10":"application/pkcs10",".p12":"application/x-pkcs12",".p7b":"application/x-pkcs7-certificates",".p7c":"application/x-pkcs7-mime",".p7m":"application/x-pkcs7-mime",".p7r":"application/x-pkcs7-certreqresp",".p7s":"application/x-pkcs7-signature",".pac":"audio/x-pac",".pae":"audio/x-epac",".pan":"application/x-pan",".pcx":"image/x-pcx",".pda":"image/x-pda",".pfr":"application/font-tdpfr",".pfx":"application/x-pkcs12",".pko":"application/ynd.ms-pkipko",".pm":"application/x-perl",".pma":"application/x-perfmon",".pmc":"application/x-perfmon",".pmd":"application/x-pmd",".pml":"application/x-perfmon",".pmr":"application/x-perfmon",".pmw":"application/x-perfmon",".pnz":"image/png",".pot,":"application/vnd.ms-powerpoint",".pps":"application/vnd.ms-powerpoint",".pqf":"application/x-cprplayer",".pqi":"application/cprplayer",".prc":"application/x-prc",".prf":"application/pics-rules",".prop":"text/plain",".proxy":"application/x-ns-proxy-autoconfig",".ptlk":"application/listenup",".pub":"application/x-mspublisher",".pvx":"video/x-pv-pvx",".qcp":"audio/vnd.qcelp",".r3t":"text/vnd.rn-realtext3d",".rar":"application/octet-stream",".rc":"text/plain",".rf":"image/vnd.rn-realflash",".rlf":"application/x-richlink",".rmf":"audio/x-rmf",".rmi":"audio/mid",".rmm":"audio/x-pn-realaudio",".rmvb":"audio/x-pn-realaudio",".rnx":"application/vnd.rn-realplayer",".rp":"image/vnd.rn-realpix",".rt":"text/vnd.rn-realtext",".rte":"x-lml/x-gps",".rtg":"application/metastream",".rv":"video/vnd.rn-realvideo",".rwc":"application/x-rogerwilco",".s3m":"audio/x-mod",".s3z":"audio/x-mod",".sca":"application/x-supercard",".scd":"application/x-msschedule",".sct":"text/scriptlet",".sdf":"application/e-score",".sea":"application/x-stuffit",".setpay":"application/set-payment_old-initiation",".setreg":"application/set-registration-initiation",".shtml":"text/html",".shtm":"text/html",".shw":"application/presentations",".si6":"image/si6",".si7":"image/vnd.stiwap.sis",".si9":"image/vnd.lgtwap.sis",".slc":"application/x-salsa",".smd":"audio/x-smd",".smp":"application/studiom",".smz":"audio/x-smd",".spc":"application/x-pkcs7-certificates",".spr":"application/x-sprite",".sprite":"application/x-sprite",".sdp":"application/sdp",".spt":"application/x-spt",".sst":"application/vnd.ms-pkicertstore",".stk":"application/hyperstudio",".stl":"application/vnd.ms-pkistl",".stm":"text/html",".svf":"image/vnd",".svh":"image/svh",".svr":"x-world/x-svr",".swfl":"application/x-shockwave-flash",".tad":"application/octet-stream",".talk":"text/x-speech",".taz":"application/x-tar",".tbp":"application/x-timbuktu",".tbt":"application/x-timbuktu",".tgz":"application/x-compressed",".thm":"application/vnd.eri.thm",".tki":"application/x-tkined",".tkined":"application/x-tkined",".toc":"application/toc",".toy":"image/toy",".trk":"x-lml/x-gps",".trm":"application/x-msterminal",".tsi":"audio/tsplayer",".tsp":"application/dsptype",".ttf":"application/octet-stream",".ttz":"application/t-time",".uls":"text/iuls",".ult":"audio/x-mod",".uu":"application/x-uuencode",".uue":"application/x-uuencode",".vcf":"text/x-vcard",".vdo":"video/vdo",".vib":"audio/vib",".viv":"video/vivo",".vivo":"video/vivo",".vmd":"application/vocaltec-media-desc",".vmf":"application/vocaltec-media-file",".vmi":"application/x-dreamcast-vms-info",".vms":"application/x-dreamcast-vms",".vox":"audio/voxware",".vqe":"audio/x-twinvq-plugin",".vqf":"audio/x-twinvq",".vql":"audio/x-twinvq",".vre":"x-world/x-vream",".vrt":"x-world/x-vrt",".vrw":"x-world/x-vream",".vts":"workbook/formulaone",".wcm":"application/vnd.ms-works",".wdb":"application/vnd.ms-works",".web":"application/vnd.xara",".wi":"image/wavelet",".wis":"application/x-InstallShield",".wks":"application/vnd.ms-works",".wmd":"application/x-ms-wmd",".wmf":"application/x-msmetafile",".wmlscript":"text/vnd.wap.wmlscript",".wmz":"application/x-ms-wmz",".wpng":"image/x-up-wpng",".wps":"application/vnd.ms-works",".wpt":"x-lml/x-gps",".wri":"application/x-mswrite",".wrz":"x-world/x-vrml",".ws":"text/vnd.wap.wmlscript",".wsc":"application/vnd.wap.wmlscriptc",".wv":"video/wavelet",".wxl":"application/x-wxl",".x-gzip":"application/x-gzip",".xaf":"x-world/x-vrml",".xar":"application/vnd.xara",".xdm":"application/x-xdma",".xdma":"application/x-xdma",".xdw":"application/vnd.fujixerox.docuworks",".xhtm":"application/xhtml+xml",".xla":"application/vnd.ms-excel",".xlc":"application/vnd.ms-excel",".xll":"application/x-excel",".xlm":"application/vnd.ms-excel",".xlt":"application/vnd.ms-excel",".xlw":"application/vnd.ms-excel",".xm":"audio/x-mod",".xmz":"audio/x-mod",".xof":"x-world/x-vrml",".xpi":"application/x-xpinstall",".xsit":"text/xml",".yz1":"application/x-yz1",".z":"application/x-compress",".zac":"application/x-zaurus-zac",".json":"application/json"}`, - "file_viewers": `[{"viewers":[{"id":"music","type":"builtin","action":"view","display_name":"fileManager.musicPlayer","exts":["mp3","ogg","wav","flac","m4a"]},{"id":"epub","type":"builtin","action":"view","display_name":"fileManager.epubViewer","exts":["epub"]},{"id":"googledocs","type":"custom","action":"view","display_name":"fileManager.googledocs","icon":"/static/img/viewers/gdrive.png","url":"https://docs.google.com/gview?url={$src}&embedded=true","exts":["jpeg","png","gif","tiff","bmp","webm","mpeg4","3gpp","mov","avi","mpegps","wmv","flv","txt","css","html","php","c","cpp","h","hpp","js","doc","docx","xls","xlsx","ppt","pptx","pdf","pages","ai","psd","tiff","dxf","svg","eps","ps","ttf","xps"],"max_size":26214400},{"id":"m365online","type":"custom","action":"view","display_name":"fileManager.m365viewer","icon":"/static/img/viewers/m365.svg","url":"https://view.officeapps.live.com/op/view.aspx?src={$src}","exts":["doc","docx","docm","dotm","dotx","xlsx","xlsb","xls","xlsm","pptx","ppsx","ppt","pps","pptm","potm","ppam","potx","ppsm"],"max_size":10485760},{"id":"pdf","type":"builtin","action":"view","display_name":"fileManager.pdfViewer","exts":["pdf"]},{"id":"video","type":"builtin","action":"view","icon":"/static/img/viewers/artplayer.png","display_name":"Artplayer","exts":["mp4","mkv","webm","avi","m3u8","mov","flv"]},{"id":"markdown","type":"builtin","action":"edit","display_name":"fileManager.markdownEditor","exts":["md"],"templates":[{"ext":"md","display_name":"Markdown"}]},{"id":"drawio","type":"builtin","action":"edit","icon":"/static/img/viewers/drawio.svg","display_name":"draw.io","exts":["drawio","dwb"],"props":{"host":"https://embed.diagrams.net"},"templates":[{"ext":"drawio","display_name":"fileManager.diagram"},{"ext":"dwb","display_name":"fileManager.whiteboard"}]},{"id":"image","type":"builtin","action":"edit","display_name":"fileManager.imageViewer","exts":["bmp","png","gif","jpg","jpeg","svg","webp","heic","heif"]},{"id":"monaco","type":"builtin","action":"edit","icon":"/static/img/viewers/monaco.svg","display_name":"fileManager.monacoEditor","exts":["md","txt","json","php","py","bat","c","h","cpp","hpp","cs","css","dockerfile","go","html","htm","ini","java","js","jsx","less","lua","sh","sql","xml","yaml"],"templates":[{"ext":"txt","display_name":"fileManager.text"}]},{"id":"photopea","type":"builtin","icon":"/static/img/viewers/photopea.png","action":"edit","display_name":"Photopea","exts":["psd","ai","indd","xcf","xd","fig","kri","clip","pxd","pxz","cdr","ufo","afphoyo","svg","esp","pdf","pdn","wmf","emf","png","jpg","jpeg","gif","webp","ico","icns","bmp","avif","heic","jxl","ppm","pgm","pbm","tiff","dds","iff","anim","tga","dng","nef","cr2","cr3","arw","rw2","raf","orf","gpr","3fr","fff"]},{"id":"excalidraw","type":"builtin","action":"view","icon":"/static/img/viewers/excalidraw.svg","display_name":"Excalidraw","exts":["excalidraw"],"templates":[{"ext":"excalidraw","display_name":"Excalidraw"}]}]}]`, "logto_enabled": "0", "logto_config": `{"direct_sign_in":true,"display_name":"vas.sso"}`, "qq_login": `0`, "qq_login_config": `{"direct_sign_in":false}`, "license": "", } + +func init() { + explorerIcons, err := json.Marshal(defaultIcons) + if err != nil { + panic(err) + } + DefaultSettings["explorer_icons"] = string(explorerIcons) + + viewers, err := json.Marshal(defaultFileViewers) + if err != nil { + panic(err) + } + + DefaultSettings["file_viewers"] = string(viewers) +} diff --git a/inventory/types/types.go b/inventory/types/types.go index 3cadc33b..2e6b9df8 100644 --- a/inventory/types/types.go +++ b/inventory/types/types.go @@ -178,6 +178,14 @@ type ( // Whether to share view setting from owner ShareView bool `json:"share_view,omitempty"` } + + FileTypeIconSetting struct { + Exts []string `json:"exts"` + Icon string `json:"icon,omitempty"` + Color string `json:"color,omitempty"` + ColorDark string `json:"color_dark,omitempty"` + Img string `json:"img,omitempty"` + } ) const ( @@ -250,3 +258,40 @@ const ( DownloaderProviderAria2 = DownloaderProvider("aria2") DownloaderProviderQBittorrent = DownloaderProvider("qbittorrent") ) + +type ( + ViewerAction string + ViewerType string +) + +const ( + ViewerActionView = "view" + ViewerActionEdit = "edit" + + ViewerTypeBuiltin = "builtin" + ViewerTypeWopi = "wopi" + ViewerTypeCustom = "custom" +) + +type Viewer struct { + ID string `json:"id"` + Type ViewerType `json:"type"` + DisplayName string `json:"display_name"` + Exts []string `json:"exts"` + Url string `json:"url,omitempty"` + Icon string `json:"icon,omitempty"` + WopiActions map[string]map[ViewerAction]string `json:"wopi_actions,omitempty"` + Props map[string]string `json:"props,omitempty"` + MaxSize int64 `json:"max_size,omitempty"` + Disabled bool `json:"disabled,omitempty"` + Templates []NewFileTemplate `json:"templates,omitempty"` +} + +type ViewerGroup struct { + Viewers []Viewer `json:"viewers"` +} + +type NewFileTemplate struct { + Ext string `json:"ext"` + DisplayName string `json:"display_name"` +} diff --git a/middleware/wopi.go b/middleware/wopi.go index 0d46866a..8e498e1d 100644 --- a/middleware/wopi.go +++ b/middleware/wopi.go @@ -2,9 +2,9 @@ package middleware import ( "github.com/cloudreve/Cloudreve/v4/application/dependency" + "github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager" "github.com/cloudreve/Cloudreve/v4/pkg/hashid" - "github.com/cloudreve/Cloudreve/v4/pkg/setting" "github.com/cloudreve/Cloudreve/v4/pkg/util" "github.com/cloudreve/Cloudreve/v4/pkg/wopi" "github.com/gin-gonic/gin" @@ -67,7 +67,7 @@ func ViewerSessionValidation() gin.HandlerFunc { // Check if the viewer is still available viewers := settings.FileViewers(c) - var v *setting.Viewer + var v *types.Viewer for _, group := range viewers { for _, viewer := range group.Viewers { if viewer.ID == session.ViewerID && !viewer.Disabled { diff --git a/pkg/filemanager/manager/manager.go b/pkg/filemanager/manager/manager.go index 5dcda169..6cce600c 100644 --- a/pkg/filemanager/manager/manager.go +++ b/pkg/filemanager/manager/manager.go @@ -54,7 +54,7 @@ type ( // UpsertMedata update or insert metadata of given file PatchMedata(ctx context.Context, path []*fs.URI, data ...fs.MetadataPatch) error // CreateViewerSession creates a viewer session for given file - CreateViewerSession(ctx context.Context, uri *fs.URI, version string, viewer *setting.Viewer) (*ViewerSession, error) + CreateViewerSession(ctx context.Context, uri *fs.URI, version string, viewer *types.Viewer) (*ViewerSession, error) // TraverseFile traverses a file to its root file, return the file with linked root. TraverseFile(ctx context.Context, fileID int) (fs.File, error) } diff --git a/pkg/filemanager/manager/viewer.go b/pkg/filemanager/manager/viewer.go index 819ef310..30fd8a61 100644 --- a/pkg/filemanager/manager/viewer.go +++ b/pkg/filemanager/manager/viewer.go @@ -9,7 +9,6 @@ import ( "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/setting" "github.com/cloudreve/Cloudreve/v4/pkg/util" "github.com/gofrs/uuid" ) @@ -44,7 +43,7 @@ func init() { gob.Register(ViewerSessionCache{}) } -func (m *manager) CreateViewerSession(ctx context.Context, uri *fs.URI, version string, viewer *setting.Viewer) (*ViewerSession, error) { +func (m *manager) CreateViewerSession(ctx context.Context, uri *fs.URI, version string, viewer *types.Viewer) (*ViewerSession, error) { file, err := m.fs.Get(ctx, uri, dbfs.WithFileEntities(), dbfs.WithNotRoot()) if err != nil { return nil, err @@ -88,6 +87,6 @@ func ViewerSessionFromContext(ctx context.Context) *ViewerSessionCache { return ctx.Value(ViewerSessionCacheCtx{}).(*ViewerSessionCache) } -func ViewerFromContext(ctx context.Context) *setting.Viewer { - return ctx.Value(ViewerCtx{}).(*setting.Viewer) +func ViewerFromContext(ctx context.Context) *types.Viewer { + return ctx.Value(ViewerCtx{}).(*types.Viewer) } diff --git a/pkg/setting/provider.go b/pkg/setting/provider.go index 2b379222..b3005e34 100644 --- a/pkg/setting/provider.go +++ b/pkg/setting/provider.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/cloudreve/Cloudreve/v4/inventory/types" "net/url" "strconv" "strings" @@ -169,7 +170,7 @@ type ( // FolderPropsCacheTTL returns the cache TTL of folder summary. FolderPropsCacheTTL(ctx context.Context) int // FileViewers returns the file viewers settings. - FileViewers(ctx context.Context) []ViewerGroup + FileViewers(ctx context.Context) []types.ViewerGroup // ViewerSessionTTL returns the TTL of viewer session. ViewerSessionTTL(ctx context.Context) int // MimeMapping returns the extension to MIME mapping settings. @@ -232,11 +233,11 @@ func (s *settingProvider) Avatar(ctx context.Context) *Avatar { } } -func (s *settingProvider) FileViewers(ctx context.Context) []ViewerGroup { +func (s *settingProvider) FileViewers(ctx context.Context) []types.ViewerGroup { raw := s.getString(ctx, "file_viewers", "[]") - var viewers []ViewerGroup + var viewers []types.ViewerGroup if err := json.Unmarshal([]byte(raw), &viewers); err != nil { - return []ViewerGroup{} + return []types.ViewerGroup{} } return viewers diff --git a/pkg/setting/types.go b/pkg/setting/types.go index d3e4d5ef..a31337a3 100644 --- a/pkg/setting/types.go +++ b/pkg/setting/types.go @@ -176,42 +176,6 @@ type MapSetting struct { // Viewer related -type ( - ViewerAction string - ViewerType string -) - -const ( - ViewerActionView = "view" - ViewerActionEdit = "edit" - - ViewerTypeBuiltin = "builtin" - ViewerTypeWopi = "wopi" -) - -type Viewer struct { - ID string `json:"id"` - Type ViewerType `json:"type"` - DisplayName string `json:"display_name"` - Exts []string `json:"exts"` - Url string `json:"url,omitempty"` - Icon string `json:"icon,omitempty"` - WopiActions map[string]map[ViewerAction]string `json:"wopi_actions,omitempty"` - Props map[string]string `json:"props,omitempty"` - MaxSize int64 `json:"max_size,omitempty"` - Disabled bool `json:"disabled,omitempty"` - Templates []NewFileTemplate `json:"templates,omitempty"` -} - -type ViewerGroup struct { - Viewers []Viewer `json:"viewers"` -} - -type NewFileTemplate struct { - Ext string `json:"ext"` - DisplayName string `json:"display_name"` -} - type ( SearchCategory string ) diff --git a/pkg/wopi/discovery.go b/pkg/wopi/discovery.go index 64117309..42918c87 100644 --- a/pkg/wopi/discovery.go +++ b/pkg/wopi/discovery.go @@ -3,7 +3,7 @@ package wopi import ( "encoding/xml" "fmt" - "github.com/cloudreve/Cloudreve/v4/pkg/setting" + "github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/gofrs/uuid" "github.com/samber/lo" ) @@ -16,23 +16,23 @@ var ( ActionEdit = ActonType("edit") ) -func DiscoveryXmlToViewerGroup(xmlStr string) (*setting.ViewerGroup, error) { +func DiscoveryXmlToViewerGroup(xmlStr string) (*types.ViewerGroup, error) { var discovery WopiDiscovery if err := xml.Unmarshal([]byte(xmlStr), &discovery); err != nil { return nil, fmt.Errorf("failed to parse WOPI discovery XML: %w", err) } - group := &setting.ViewerGroup{ - Viewers: make([]setting.Viewer, 0, len(discovery.NetZone.App)), + group := &types.ViewerGroup{ + Viewers: make([]types.Viewer, 0, len(discovery.NetZone.App)), } for _, app := range discovery.NetZone.App { - viewer := setting.Viewer{ + viewer := types.Viewer{ ID: uuid.Must(uuid.NewV4()).String(), DisplayName: app.Name, - Type: setting.ViewerTypeWopi, + Type: types.ViewerTypeWopi, Icon: app.FavIconUrl, - WopiActions: make(map[string]map[setting.ViewerAction]string), + WopiActions: make(map[string]map[types.ViewerAction]string), } for _, action := range app.Action { @@ -41,21 +41,21 @@ func DiscoveryXmlToViewerGroup(xmlStr string) (*setting.ViewerGroup, error) { } if _, ok := viewer.WopiActions[action.Ext]; !ok { - viewer.WopiActions[action.Ext] = make(map[setting.ViewerAction]string) + viewer.WopiActions[action.Ext] = make(map[types.ViewerAction]string) } if action.Name == string(ActionPreview) { - viewer.WopiActions[action.Ext][setting.ViewerActionView] = action.Urlsrc + viewer.WopiActions[action.Ext][types.ViewerActionView] = action.Urlsrc } else if action.Name == string(ActionPreviewFallback) { - viewer.WopiActions[action.Ext][setting.ViewerActionView] = action.Urlsrc + viewer.WopiActions[action.Ext][types.ViewerActionView] = action.Urlsrc } else if action.Name == string(ActionEdit) { - viewer.WopiActions[action.Ext][setting.ViewerActionEdit] = action.Urlsrc + viewer.WopiActions[action.Ext][types.ViewerActionEdit] = action.Urlsrc } else if len(viewer.WopiActions[action.Ext]) == 0 { delete(viewer.WopiActions, action.Ext) } } - viewer.Exts = lo.MapToSlice(viewer.WopiActions, func(key string, value map[setting.ViewerAction]string) string { + viewer.Exts = lo.MapToSlice(viewer.WopiActions, func(key string, value map[types.ViewerAction]string) string { return key }) diff --git a/pkg/wopi/wopi.go b/pkg/wopi/wopi.go index c1589874..9664afba 100644 --- a/pkg/wopi/wopi.go +++ b/pkg/wopi/wopi.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "github.com/cloudreve/Cloudreve/v4/inventory/types" "net/url" "strings" "time" @@ -56,7 +57,7 @@ const ( LockDuration = time.Duration(30) * time.Minute ) -func GenerateWopiSrc(ctx context.Context, action setting.ViewerAction, viewer *setting.Viewer, viewerSession *manager.ViewerSession) (*url.URL, error) { +func GenerateWopiSrc(ctx context.Context, action types.ViewerAction, viewer *types.Viewer, viewerSession *manager.ViewerSession) (*url.URL, error) { dep := dependency.FromContext(ctx) base := dep.SettingProvider().SiteURL(setting.UseFirstSiteUrl(ctx)) hasher := dep.HashIDEncoder() @@ -69,7 +70,7 @@ func GenerateWopiSrc(ctx context.Context, action setting.ViewerAction, viewer *s var ( src string ) - fallbackOrder := []setting.ViewerAction{action, setting.ViewerActionView, setting.ViewerActionEdit} + fallbackOrder := []types.ViewerAction{action, types.ViewerActionView, types.ViewerActionEdit} for _, a := range fallbackOrder { if src, ok = availableActions[a]; ok { break diff --git a/service/admin/tools.go b/service/admin/tools.go index 20dc90f1..9409b949 100644 --- a/service/admin/tools.go +++ b/service/admin/tools.go @@ -12,6 +12,7 @@ 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" ) @@ -107,7 +108,7 @@ type ( FetchWOPIDiscoveryParamCtx struct{} ) -func (s *FetchWOPIDiscoveryService) Fetch(c *gin.Context) (*setting.ViewerGroup, error) { +func (s *FetchWOPIDiscoveryService) Fetch(c *gin.Context) (*types.ViewerGroup, error) { dep := dependency.FromContext(c) requestClient := dep.RequestClient(request2.WithContext(c), request2.WithLogger(dep.Logger())) content, err := requestClient.Request("GET", s.Endpoint, nil).CheckHTTPResponse(http.StatusOK).GetResponse() diff --git a/service/basic/site.go b/service/basic/site.go index d291a256..7a54d157 100644 --- a/service/basic/site.go +++ b/service/basic/site.go @@ -3,6 +3,7 @@ package basic import ( "github.com/cloudreve/Cloudreve/v4/application/dependency" "github.com/cloudreve/Cloudreve/v4/inventory" + "github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/pkg/setting" "github.com/cloudreve/Cloudreve/v4/service/user" "github.com/gin-gonic/gin" @@ -39,7 +40,7 @@ type SiteConfig struct { EmojiPreset string `json:"emoji_preset,omitempty"` MapProvider setting.MapProvider `json:"map_provider,omitempty"` GoogleMapTileType setting.MapGoogleTileType `json:"google_map_tile_type,omitempty"` - FileViewers []setting.ViewerGroup `json:"file_viewers,omitempty"` + FileViewers []types.ViewerGroup `json:"file_viewers,omitempty"` MaxBatchSize int `json:"max_batch_size,omitempty"` ThumbnailWidth int `json:"thumbnail_width,omitempty"` ThumbnailHeight int `json:"thumbnail_height,omitempty"` diff --git a/service/explorer/viewer.go b/service/explorer/viewer.go index d8ebc889..f8fd01b1 100644 --- a/service/explorer/viewer.go +++ b/service/explorer/viewer.go @@ -21,7 +21,6 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource" "github.com/cloudreve/Cloudreve/v4/pkg/hashid" "github.com/cloudreve/Cloudreve/v4/pkg/serializer" - "github.com/cloudreve/Cloudreve/v4/pkg/setting" "github.com/cloudreve/Cloudreve/v4/pkg/wopi" "github.com/gin-gonic/gin" ) @@ -371,7 +370,7 @@ type ( Uri string `json:"uri" form:"uri" binding:"required"` Version string `json:"version" form:"version"` ViewerID string `json:"viewer_id" form:"viewer_id" binding:"required"` - PreferredAction setting.ViewerAction `json:"preferred_action" form:"preferred_action" binding:"required"` + PreferredAction types.ViewerAction `json:"preferred_action" form:"preferred_action" binding:"required"` } CreateViewerSessionParamCtx struct{} ) @@ -389,7 +388,7 @@ func (s *CreateViewerSessionService) Create(c *gin.Context) (*ViewerSessionRespo // Find the given viewer viewers := dep.SettingProvider().FileViewers(c) - var targetViewer *setting.Viewer + var targetViewer *types.Viewer for _, group := range viewers { for _, viewer := range group.Viewers { if viewer.ID == s.ViewerID && !viewer.Disabled { @@ -413,7 +412,7 @@ func (s *CreateViewerSessionService) Create(c *gin.Context) (*ViewerSessionRespo } res := &ViewerSessionResponse{Session: viewerSession} - if targetViewer.Type == setting.ViewerTypeWopi { + if targetViewer.Type == types.ViewerTypeWopi { // For WOPI viewer, generate WOPI src wopiSrc, err := wopi.GenerateWopiSrc(c, s.PreferredAction, targetViewer, viewerSession) if err != nil { From 1bd62e8feb3a0cbfd513c8d39473baa5313553bb Mon Sep 17 00:00:00 2001 From: charlieJ107 <42380833+charlieJ107@users.noreply.github.com> Date: Mon, 23 Jun 2025 10:12:20 +0100 Subject: [PATCH 06/27] [Feature](database): Add Support for SSL Connections and Database URL Configuration (#2540) * feat(database): add support for SSL connections and database URL configuration * feat(config): update Redis configuration to use TLS in configurre name instead of SSL * fix(database): remove default values for DatabaseURL and SSLMode in DatabaseConfig * chore(.gitignore): add cloudreve built binary to ignore list --- .gitignore | 4 +- application/dependency/dependency.go | 6 +-- inventory/client.go | 79 ++++++++++++++++------------ pkg/cache/redis.go | 18 ++++--- pkg/conf/conf.go | 5 +- pkg/conf/types.go | 35 +++++++----- 6 files changed, 85 insertions(+), 62 deletions(-) diff --git a/.gitignore b/.gitignore index 3b2e45c2..d8a123e0 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,6 @@ conf/conf.ini dist/ data/ -tmp/ \ No newline at end of file +tmp/ +.devcontainer/ +cloudreve diff --git a/application/dependency/dependency.go b/application/dependency/dependency.go index 300fb8a0..70d8514b 100644 --- a/application/dependency/dependency.go +++ b/application/dependency/dependency.go @@ -329,11 +329,7 @@ func (d *dependency) KV() cache.Driver { d.kv = cache.NewRedisStore( d.Logger(), 10, - config.Network, - config.Server, - config.User, - config.Password, - config.DB, + config, ) } else { d.kv = cache.NewMemoStore(util.DataPath(cache.DefaultCacheFile), d.Logger()) diff --git a/inventory/client.go b/inventory/client.go index 6e3d2934..52420538 100644 --- a/inventory/client.go +++ b/inventory/client.go @@ -58,45 +58,56 @@ func NewRawEntClient(l logging.Logger, config conf.ConfigProvider) (*ent.Client, client *sql.Driver ) - switch confDBType { - case conf.SQLiteDB: - dbFile := util.RelativePath(dbConfig.DBFile) - l.Info("Connect to SQLite database %q.", dbFile) - client, err = sql.Open("sqlite3", util.RelativePath(dbConfig.DBFile)) - case conf.PostgresDB: - l.Info("Connect to Postgres database %q.", dbConfig.Host) - client, err = sql.Open("postgres", fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable", - dbConfig.Host, - dbConfig.User, - dbConfig.Password, - dbConfig.Name, - dbConfig.Port)) - case conf.MySqlDB, conf.MsSqlDB: - l.Info("Connect to MySQL/SQLServer database %q.", dbConfig.Host) - var host string - if dbConfig.UnixSocket { - host = fmt.Sprintf("unix(%s)", - dbConfig.Host) - } else { - host = fmt.Sprintf("(%s:%d)", + // Check if the database type is supported. + if confDBType != conf.SQLiteDB && confDBType != conf.MySqlDB && confDBType != conf.PostgresDB { + return nil, fmt.Errorf("unsupported database type: %s", confDBType) + } + // If Database connection string provided, use it directly. + if dbConfig.DatabaseURL != "" { + l.Info("Connect to database with connection string %q.", dbConfig.DatabaseURL) + client, err = sql.Open(string(confDBType), dbConfig.DatabaseURL) + } else { + + switch confDBType { + case conf.SQLiteDB: + dbFile := util.RelativePath(dbConfig.DBFile) + l.Info("Connect to SQLite database %q.", dbFile) + client, err = sql.Open("sqlite3", util.RelativePath(dbConfig.DBFile)) + case conf.PostgresDB: + l.Info("Connect to Postgres database %q.", dbConfig.Host) + client, err = sql.Open("postgres", fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=allow", dbConfig.Host, - dbConfig.Port) + dbConfig.User, + dbConfig.Password, + dbConfig.Name, + dbConfig.Port)) + case conf.MySqlDB, conf.MsSqlDB: + l.Info("Connect to MySQL/SQLServer database %q.", dbConfig.Host) + var host string + if dbConfig.UnixSocket { + host = fmt.Sprintf("unix(%s)", + dbConfig.Host) + } else { + host = fmt.Sprintf("(%s:%d)", + dbConfig.Host, + dbConfig.Port) + } + + client, err = sql.Open(string(confDBType), fmt.Sprintf("%s:%s@%s/%s?charset=%s&parseTime=True&loc=Local", + dbConfig.User, + dbConfig.Password, + host, + dbConfig.Name, + dbConfig.Charset)) + default: + return nil, fmt.Errorf("unsupported database type %q", confDBType) } - client, err = sql.Open(string(confDBType), fmt.Sprintf("%s:%s@%s/%s?charset=%s&parseTime=True&loc=Local", - dbConfig.User, - dbConfig.Password, - host, - dbConfig.Name, - dbConfig.Charset)) - default: - return nil, fmt.Errorf("unsupported database type %q", confDBType) - } + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } - if err != nil { - return nil, fmt.Errorf("failed to open database: %w", err) } - // Set connection pool db := client.DB() db.SetMaxIdleConns(50) diff --git a/pkg/cache/redis.go b/pkg/cache/redis.go index 0cdee1a3..d5647681 100644 --- a/pkg/cache/redis.go +++ b/pkg/cache/redis.go @@ -3,10 +3,12 @@ package cache import ( "bytes" "encoding/gob" - "github.com/cloudreve/Cloudreve/v4/pkg/logging" "strconv" "time" + "github.com/cloudreve/Cloudreve/v4/pkg/conf" + "github.com/cloudreve/Cloudreve/v4/pkg/logging" + "github.com/gomodule/redigo/redis" ) @@ -44,7 +46,7 @@ func deserializer(value []byte) (any, error) { } // NewRedisStore 创建新的redis存储 -func NewRedisStore(l logging.Logger, size int, network, address, user, password, database string) *RedisStore { +func NewRedisStore(l logging.Logger, size int, redisConfig *conf.Redis) *RedisStore { return &RedisStore{ pool: &redis.Pool{ MaxIdle: size, @@ -54,17 +56,19 @@ func NewRedisStore(l logging.Logger, size int, network, address, user, password, return err }, Dial: func() (redis.Conn, error) { - db, err := strconv.Atoi(database) + db, err := strconv.Atoi(redisConfig.DB) if err != nil { return nil, err } c, err := redis.Dial( - network, - address, + redisConfig.Network, + redisConfig.Server, redis.DialDatabase(db), - redis.DialPassword(password), - redis.DialUsername(user), + redis.DialPassword(redisConfig.Password), + redis.DialUsername(redisConfig.User), + redis.DialUseTLS(redisConfig.UseTLS), + redis.DialTLSSkipVerify(redisConfig.TLSSkipVerify), ) if err != nil { l.Panic("Failed to create Redis connection: %s", err) diff --git a/pkg/conf/conf.go b/pkg/conf/conf.go index 78e770b7..372f68ef 100644 --- a/pkg/conf/conf.go +++ b/pkg/conf/conf.go @@ -2,12 +2,13 @@ package conf import ( "fmt" + "os" + "strings" + "github.com/cloudreve/Cloudreve/v4/pkg/logging" "github.com/cloudreve/Cloudreve/v4/pkg/util" "github.com/go-ini/ini" "github.com/go-playground/validator/v10" - "os" - "strings" ) const ( diff --git a/pkg/conf/types.go b/pkg/conf/types.go index 32e2e4f3..285278fb 100644 --- a/pkg/conf/types.go +++ b/pkg/conf/types.go @@ -24,6 +24,10 @@ type Database struct { Port int Charset string UnixSocket bool + // 允许直接使用DATABASE_URL来配置数据库连接 + DatabaseURL string + // SSLMode 允许使用SSL连接数据库, 用户可以在sslmode string中添加证书等配置 + SSLMode string } type SysMode string @@ -65,11 +69,13 @@ type Slave struct { // Redis 配置 type Redis struct { - Network string - Server string - User string - Password string - DB string + Network string + Server string + User string + Password string + DB string + UseTLS bool + TLSSkipVerify bool } // 跨域配置 @@ -85,18 +91,21 @@ type Cors struct { // RedisConfig Redis服务器配置 var RedisConfig = &Redis{ - Network: "tcp", - Server: "", - Password: "", - DB: "0", + Network: "tcp", + Server: "", + Password: "", + DB: "0", + UseTLS: false, + TLSSkipVerify: true, } // DatabaseConfig 数据库配置 var DatabaseConfig = &Database{ - Charset: "utf8mb4", - DBFile: util.DataPath("cloudreve.db"), - Port: 3306, - UnixSocket: false, + Charset: "utf8mb4", + DBFile: util.DataPath("cloudreve.db"), + Port: 3306, + UnixSocket: false, + DatabaseURL: "", } // SystemConfig 系统公用配置 From b11188fa504a39a69cc3d8fdbada612e1a73dc8c Mon Sep 17 00:00:00 2001 From: WittF Date: Mon, 23 Jun 2025 17:16:29 +0800 Subject: [PATCH 07/27] feat(file): add support for more file extensions (#2557) - Add aac audio format support - Add ini, env, json, log, yml text file extensions - Add iso archive format support - Add ico, icns thumbnail generation support --- inventory/setting.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/inventory/setting.go b/inventory/setting.go index 45b5e8c4..b9139ada 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -82,7 +82,7 @@ func (c *settingClient) Set(ctx context.Context, settings map[string]string) err var ( defaultIcons = []types.FileTypeIconSetting{ { - Exts: []string{"mp3", "flac", "ape", "wav", "acc", "ogg", "m4a"}, + Exts: []string{"mp3", "flac", "ape", "wav", "acc", "ogg", "m4a", "aac"}, Icon: "audio", Color: "#651fff", }, @@ -122,7 +122,7 @@ var ( Icon: "excel", }, { - Exts: []string{"txt", "html"}, + Exts: []string{"txt", "html", "ini", "env", "json", "log", "yml"}, Color: "#607d8b", Icon: "text", }, @@ -132,7 +132,7 @@ var ( Icon: "torrent", }, { - Exts: []string{"zip", "gz", "xz", "tar", "rar", "7z", "bz2", "z"}, + Exts: []string{"zip", "gz", "xz", "tar", "rar", "7z", "bz2", "z", "iso"}, Color: "#f9a825", Icon: "zip", }, @@ -211,7 +211,7 @@ var ( ID: "music", Type: types.ViewerTypeBuiltin, DisplayName: "fileManager.musicPlayer", - Exts: []string{"mp3", "ogg", "wav", "flac", "m4a"}, + Exts: []string{"mp3", "ogg", "wav", "flac", "m4a", "aac"}, }, { ID: "epub", @@ -293,7 +293,7 @@ var ( Type: types.ViewerTypeBuiltin, Icon: "/static/img/viewers/monaco.svg", DisplayName: "fileManager.monacoEditor", - Exts: []string{"md", "txt", "json", "php", "py", "bat", "c", "h", "cpp", "hpp", "cs", "css", "dockerfile", "go", "html", "htm", "ini", "java", "js", "jsx", "less", "lua", "sh", "sql", "xml", "yaml"}, + Exts: []string{"md", "txt", "json", "php", "py", "bat", "c", "h", "cpp", "hpp", "cs", "css", "dockerfile", "go", "html", "htm", "ini", "java", "js", "jsx", "less", "lua", "sh", "sql", "xml", "yaml", "ts", "tsx", "yml", "vue", "env", "log"}, Templates: []types.NewFileTemplate{ { Ext: "txt", @@ -406,7 +406,7 @@ var DefaultSettings = map[string]string{ "thumb_builtin_max_size": "78643200", // 75 MB "thumb_vips_max_size": "78643200", // 75 MB "thumb_vips_enabled": "0", - "thumb_vips_exts": "3fr,ari,arw,bay,braw,crw,cr2,cr3,cap,data,dcs,dcr,dng,drf,eip,erf,fff,gpr,iiq,k25,kdc,mdc,mef,mos,mrw,nef,nrw,obm,orf,pef,ptx,pxn,r3d,raf,raw,rwl,rw2,rwz,sr2,srf,srw,tif,x3f,csv,mat,img,hdr,pbm,pgm,ppm,pfm,pnm,svg,svgz,j2k,jp2,jpt,j2c,jpc,gif,png,jpg,jpeg,jpe,webp,tif,tiff,fits,fit,fts,exr,jxl,pdf,heic,heif,avif,svs,vms,vmu,ndpi,scn,mrxs,svslide,bif,raw", + "thumb_vips_exts": "3fr,ari,arw,bay,braw,crw,cr2,cr3,cap,data,dcs,dcr,dng,drf,eip,erf,fff,gpr,iiq,k25,kdc,mdc,mef,mos,mrw,nef,nrw,obm,orf,pef,ptx,pxn,r3d,raf,raw,rwl,rw2,rwz,sr2,srf,srw,tif,x3f,csv,mat,img,hdr,pbm,pgm,ppm,pfm,pnm,svg,svgz,j2k,jp2,jpt,j2c,jpc,gif,png,jpg,jpeg,jpe,webp,tif,tiff,fits,fit,fts,exr,jxl,pdf,heic,heif,avif,svs,vms,vmu,ndpi,scn,mrxs,svslide,bif,raw,ico,icns", "thumb_ffmpeg_enabled": "0", "thumb_vips_path": "vips", "thumb_ffmpeg_path": "ffmpeg", From d1bbfd4bc4f1c1b9f69a87a04ff74c99cfac76fa Mon Sep 17 00:00:00 2001 From: Anye <53684988+Anyexyz@users.noreply.github.com> Date: Mon, 23 Jun 2025 17:17:18 +0800 Subject: [PATCH 08/27] feat(db): add mariadb alias (#2560) --- application/migrator/model/init.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/application/migrator/model/init.go b/application/migrator/model/init.go index 46f8d362..ec00c89d 100644 --- a/application/migrator/model/init.go +++ b/application/migrator/model/init.go @@ -28,6 +28,11 @@ func Init() error { if confDBType == "sqlite3" { confDBType = "sqlite" } + + // 兼容 "mariadb" 数据库 + if confDBType == "mariadb" { + confDBType = "mysql" + } switch confDBType { case "UNSET", "sqlite": From 3db522609ef0064c0c297c2d4b90c31d5ef257e4 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 24 Jun 2025 10:47:36 +0800 Subject: [PATCH 09/27] feat(thumb): support generating thumbnails using `simple_dcraw` from LibRAW --- assets | 2 +- inventory/setting.go | 16 ++- pkg/setting/provider.go | 27 ++++- pkg/thumb/libraw.go | 261 ++++++++++++++++++++++++++++++++++++++++ pkg/thumb/pipeline.go | 8 +- pkg/thumb/tester.go | 19 +++ 6 files changed, 322 insertions(+), 11 deletions(-) create mode 100644 pkg/thumb/libraw.go diff --git a/assets b/assets index 44a696a2..0fa57541 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 44a696a2e7271bb6b98424670fe93c3df4ebc10b +Subproject commit 0fa57541d5b1018251674ef01a7e799f6899564e diff --git a/inventory/setting.go b/inventory/setting.go index b9139ada..c08a2037 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -82,7 +82,7 @@ func (c *settingClient) Set(ctx context.Context, settings map[string]string) err var ( defaultIcons = []types.FileTypeIconSetting{ { - Exts: []string{"mp3", "flac", "ape", "wav", "acc", "ogg", "m4a", "aac"}, + Exts: []string{"mp3", "flac", "ape", "wav", "acc", "ogg", "m4a"}, Icon: "audio", Color: "#651fff", }, @@ -122,7 +122,7 @@ var ( Icon: "excel", }, { - Exts: []string{"txt", "html", "ini", "env", "json", "log", "yml"}, + Exts: []string{"txt", "html"}, Color: "#607d8b", Icon: "text", }, @@ -132,7 +132,7 @@ var ( Icon: "torrent", }, { - Exts: []string{"zip", "gz", "xz", "tar", "rar", "7z", "bz2", "z", "iso"}, + Exts: []string{"zip", "gz", "xz", "tar", "rar", "7z", "bz2", "z"}, Color: "#f9a825", Icon: "zip", }, @@ -211,7 +211,7 @@ var ( ID: "music", Type: types.ViewerTypeBuiltin, DisplayName: "fileManager.musicPlayer", - Exts: []string{"mp3", "ogg", "wav", "flac", "m4a", "aac"}, + Exts: []string{"mp3", "ogg", "wav", "flac", "m4a"}, }, { ID: "epub", @@ -293,7 +293,7 @@ var ( Type: types.ViewerTypeBuiltin, Icon: "/static/img/viewers/monaco.svg", DisplayName: "fileManager.monacoEditor", - Exts: []string{"md", "txt", "json", "php", "py", "bat", "c", "h", "cpp", "hpp", "cs", "css", "dockerfile", "go", "html", "htm", "ini", "java", "js", "jsx", "less", "lua", "sh", "sql", "xml", "yaml", "ts", "tsx", "yml", "vue", "env", "log"}, + Exts: []string{"md", "txt", "json", "php", "py", "bat", "c", "h", "cpp", "hpp", "cs", "css", "dockerfile", "go", "html", "htm", "ini", "java", "js", "jsx", "less", "lua", "sh", "sql", "xml", "yaml"}, Templates: []types.NewFileTemplate{ { Ext: "txt", @@ -406,7 +406,7 @@ var DefaultSettings = map[string]string{ "thumb_builtin_max_size": "78643200", // 75 MB "thumb_vips_max_size": "78643200", // 75 MB "thumb_vips_enabled": "0", - "thumb_vips_exts": "3fr,ari,arw,bay,braw,crw,cr2,cr3,cap,data,dcs,dcr,dng,drf,eip,erf,fff,gpr,iiq,k25,kdc,mdc,mef,mos,mrw,nef,nrw,obm,orf,pef,ptx,pxn,r3d,raf,raw,rwl,rw2,rwz,sr2,srf,srw,tif,x3f,csv,mat,img,hdr,pbm,pgm,ppm,pfm,pnm,svg,svgz,j2k,jp2,jpt,j2c,jpc,gif,png,jpg,jpeg,jpe,webp,tif,tiff,fits,fit,fts,exr,jxl,pdf,heic,heif,avif,svs,vms,vmu,ndpi,scn,mrxs,svslide,bif,raw,ico,icns", + "thumb_vips_exts": "3fr,ari,arw,bay,braw,crw,cr2,cr3,cap,data,dcs,dcr,dng,drf,eip,erf,fff,gpr,iiq,k25,kdc,mdc,mef,mos,mrw,nef,nrw,obm,orf,pef,ptx,pxn,r3d,raf,raw,rwl,rw2,rwz,sr2,srf,srw,tif,x3f,csv,mat,img,hdr,pbm,pgm,ppm,pfm,pnm,svg,svgz,j2k,jp2,jpt,j2c,jpc,gif,png,jpg,jpeg,jpe,webp,tif,tiff,fits,fit,fts,exr,jxl,pdf,heic,heif,avif,svs,vms,vmu,ndpi,scn,mrxs,svslide,bif,raw", "thumb_ffmpeg_enabled": "0", "thumb_vips_path": "vips", "thumb_ffmpeg_path": "ffmpeg", @@ -420,6 +420,10 @@ var DefaultSettings = map[string]string{ "thumb_music_cover_enabled": "1", "thumb_music_cover_exts": "mp3,m4a,ogg,flac", "thumb_music_cover_max_size": "1073741824", // 1 GB + "thumb_libraw_enabled": "0", + "thumb_libraw_path": "simple_dcraw", + "thumb_libraw_max_size": "78643200", // 75 MB + "thumb_libraw_exts": "3fr,ari,arw,bay,braw,crw,cr2,cr3,cap,data,dcs,dcr,dng,drf,eip,erf,fff,gpr,iiq,k25,kdc,mdc,mef,mos,mrw,nef,nrw,obm,orf,pef,ptx,pxn,r3d,raf,raw,rwl,rw2,rwz,sr2,srf,srw,tif,x3f", "phone_required": "false", "phone_enabled": "false", "show_app_promotion": "1", diff --git a/pkg/setting/provider.go b/pkg/setting/provider.go index b3005e34..334d93d8 100644 --- a/pkg/setting/provider.go +++ b/pkg/setting/provider.go @@ -4,12 +4,13 @@ import ( "context" "encoding/json" "fmt" - "github.com/cloudreve/Cloudreve/v4/inventory/types" "net/url" "strconv" "strings" "time" + "github.com/cloudreve/Cloudreve/v4/inventory/types" + "github.com/cloudreve/Cloudreve/v4/pkg/auth/requestinfo" "github.com/cloudreve/Cloudreve/v4/pkg/boolset" ) @@ -187,6 +188,14 @@ type ( AvatarProcess(ctx context.Context) *AvatarProcess // UseFirstSiteUrl returns the first site URL. AllSiteURLs(ctx context.Context) []*url.URL + // LibRawThumbGeneratorEnabled returns true if libraw thumb generator is enabled. + LibRawThumbGeneratorEnabled(ctx context.Context) bool + // LibRawThumbMaxSize returns the maximum size of libraw thumb generator. + LibRawThumbMaxSize(ctx context.Context) int64 + // LibRawThumbExts returns the supported extensions of libraw thumb generator. + LibRawThumbExts(ctx context.Context) []string + // LibRawThumbPath returns the path of libraw executable. + LibRawThumbPath(ctx context.Context) string } UseFirstSiteUrlCtxKey = struct{} ) @@ -387,6 +396,22 @@ func (s *settingProvider) VipsPath(ctx context.Context) string { return s.getString(ctx, "thumb_vips_path", "vips") } +func (s *settingProvider) LibRawThumbGeneratorEnabled(ctx context.Context) bool { + return s.getBoolean(ctx, "thumb_libraw_enabled", false) +} + +func (s *settingProvider) LibRawThumbMaxSize(ctx context.Context) int64 { + return s.getInt64(ctx, "thumb_libraw_max_size", 78643200) +} + +func (s *settingProvider) LibRawThumbExts(ctx context.Context) []string { + return s.getStringList(ctx, "thumb_libraw_exts", []string{}) +} + +func (s *settingProvider) LibRawThumbPath(ctx context.Context) string { + return s.getString(ctx, "thumb_libraw_path", "simple_dcraw") +} + func (s *settingProvider) LibreOfficeThumbGeneratorEnabled(ctx context.Context) bool { return s.getBoolean(ctx, "thumb_libreoffice_enabled", false) } diff --git a/pkg/thumb/libraw.go b/pkg/thumb/libraw.go new file mode 100644 index 00000000..e43ba8b3 --- /dev/null +++ b/pkg/thumb/libraw.go @@ -0,0 +1,261 @@ +package thumb + +import ( + "bytes" + "context" + "errors" + "fmt" + "image" + "image/jpeg" + "image/png" + "io" + "os" + "os/exec" + "path/filepath" + + "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource" + "github.com/cloudreve/Cloudreve/v4/pkg/logging" + "github.com/cloudreve/Cloudreve/v4/pkg/setting" + "github.com/cloudreve/Cloudreve/v4/pkg/util" + "github.com/gofrs/uuid" +) + +func NewLibRawGenerator(l logging.Logger, settings setting.Provider) *LibRawGenerator { + return &LibRawGenerator{l: l, settings: settings} +} + +type LibRawGenerator struct { + l logging.Logger + settings setting.Provider +} + +func (l *LibRawGenerator) Generate(ctx context.Context, es entitysource.EntitySource, ext string, previous *Result) (*Result, error) { + if !util.IsInExtensionListExt(l.settings.LibRawThumbExts(ctx), ext) { + return nil, fmt.Errorf("unsupported video format: %w", ErrPassThrough) + } + + if es.Entity().Size() > l.settings.LibRawThumbMaxSize(ctx) { + return nil, fmt.Errorf("file is too big: %w", ErrPassThrough) + } + + // If download/copy files to temp folder + tempFolder := filepath.Join( + util.DataPath(l.settings.TempPath(ctx)), + "thumb", + fmt.Sprintf("libraw_%s", uuid.Must(uuid.NewV4()).String()), + ) + tempInputFileName := fmt.Sprintf("libraw_%s.%s", uuid.Must(uuid.NewV4()).String(), ext) + tempPath := filepath.Join(tempFolder, tempInputFileName) + tempInputFile, err := util.CreatNestedFile(tempPath) + if err != nil { + return nil, fmt.Errorf("failed to create temp file: %w", err) + } + + defer os.Remove(tempPath) + defer tempInputFile.Close() + + if _, err = io.Copy(tempInputFile, es); err != nil { + return &Result{Path: tempPath}, fmt.Errorf("failed to write input file: %w", err) + } + + tempInputFile.Close() + + cmd := exec.CommandContext(ctx, + l.settings.LibRawThumbPath(ctx), "-e", tempPath) + + // Redirect IO + var dcrawErr bytes.Buffer + cmd.Stderr = &dcrawErr + + if err := cmd.Run(); err != nil { + l.l.Warning("Failed to invoke dcraw: %s", dcrawErr.String()) + return &Result{Path: tempPath}, fmt.Errorf("failed to invoke dcraw: %w, raw output: %s", err, dcrawErr.String()) + } + + return &Result{ + Path: filepath.Join( + tempFolder, + tempInputFileName+".thumb.jpg", + ), + Continue: true, + Cleanup: []func(){func() { _ = os.RemoveAll(tempFolder) }}, + }, nil +} + +func (l *LibRawGenerator) Priority() int { + return 50 +} + +func (l *LibRawGenerator) Enabled(ctx context.Context) bool { + return l.settings.LibRawThumbGeneratorEnabled(ctx) +} + +func rotateImg(filePath string, orientation int) error { + resultImg, err := os.OpenFile(filePath, os.O_RDWR, 0777) + if err != nil { + return err + } + defer func() { _ = resultImg.Close() }() + + imgFlag := make([]byte, 3) + if _, err = io.ReadFull(resultImg, imgFlag); err != nil { + return err + } + if _, err = resultImg.Seek(0, 0); err != nil { + return err + } + + var img image.Image + if bytes.Equal(imgFlag, []byte{0xFF, 0xD8, 0xFF}) { + img, err = jpeg.Decode(resultImg) + } else { + img, err = png.Decode(resultImg) + } + if err != nil { + return err + } + + switch orientation { + case 8: + img = rotate90(img) + case 3: + img = rotate90(rotate90(img)) + case 6: + img = rotate90(rotate90(rotate90(img))) + case 2: + img = mirrorImg(img) + case 7: + img = rotate90(mirrorImg(img)) + case 4: + img = rotate90(rotate90(mirrorImg(img))) + case 5: + img = rotate90(rotate90(rotate90(mirrorImg(img)))) + } + + if err = resultImg.Truncate(0); err != nil { + return err + } + if _, err = resultImg.Seek(0, 0); err != nil { + return err + } + + if bytes.Equal(imgFlag, []byte{0xFF, 0xD8, 0xFF}) { + return jpeg.Encode(resultImg, img, nil) + } + return png.Encode(resultImg, img) +} + +func getJpegOrientation(fileName string) (int, error) { + f, err := os.Open(fileName) + if err != nil { + return 0, err + } + defer func() { _ = f.Close() }() + + header := make([]byte, 6) + defer func() { header = nil }() + if _, err = io.ReadFull(f, header); err != nil { + return 0, err + } + + // jpeg format header + if !bytes.Equal(header[:3], []byte{0xFF, 0xD8, 0xFF}) { + return 0, errors.New("not a jpeg") + } + + // not a APP1 marker + if header[3] != 0xE1 { + return 1, nil + } + + // exif data total length + totalLen := int(header[4])<<8 + int(header[5]) - 2 + buf := make([]byte, totalLen) + defer func() { buf = nil }() + if _, err = io.ReadFull(f, buf); err != nil { + return 0, err + } + + // remove Exif identifier code + buf = buf[6:] + + // byte order + parse16, parse32, err := initParseMethod(buf[:2]) + if err != nil { + return 0, err + } + + // version + _ = buf[2:4] + + // first IFD offset + offset := parse32(buf[4:8]) + + // first DE offset + offset += 2 + buf = buf[offset:] + + const ( + orientationTag = 0x112 + deEntryLength = 12 + ) + for len(buf) > deEntryLength { + tag := parse16(buf[:2]) + if tag == orientationTag { + return int(parse32(buf[8:12])), nil + } + buf = buf[deEntryLength:] + } + + return 0, errors.New("orientation not found") +} + +func initParseMethod(buf []byte) (func([]byte) int16, func([]byte) int32, error) { + if bytes.Equal(buf, []byte{0x49, 0x49}) { + return littleEndian16, littleEndian32, nil + } + if bytes.Equal(buf, []byte{0x4D, 0x4D}) { + return bigEndian16, bigEndian32, nil + } + return nil, nil, errors.New("invalid byte order") +} + +func littleEndian16(buf []byte) int16 { + return int16(buf[0]) | int16(buf[1])<<8 +} + +func bigEndian16(buf []byte) int16 { + return int16(buf[1]) | int16(buf[0])<<8 +} + +func littleEndian32(buf []byte) int32 { + return int32(buf[0]) | int32(buf[1])<<8 | int32(buf[2])<<16 | int32(buf[3])<<24 +} + +func bigEndian32(buf []byte) int32 { + return int32(buf[3]) | int32(buf[2])<<8 | int32(buf[1])<<16 | int32(buf[0])<<24 +} + +func rotate90(img image.Image) image.Image { + bounds := img.Bounds() + width, height := bounds.Dx(), bounds.Dy() + newImg := image.NewRGBA(image.Rect(0, 0, height, width)) + for x := 0; x < width; x++ { + for y := 0; y < height; y++ { + newImg.Set(y, width-x-1, img.At(x, y)) + } + } + return newImg +} + +func mirrorImg(img image.Image) image.Image { + bounds := img.Bounds() + width, height := bounds.Dx(), bounds.Dy() + newImg := image.NewRGBA(image.Rect(0, 0, width, height)) + for x := 0; x < width; x++ { + for y := 0; y < height; y++ { + newImg.Set(width-x-1, y, img.At(x, y)) + } + } + return newImg +} diff --git a/pkg/thumb/pipeline.go b/pkg/thumb/pipeline.go index bf9bbab6..0b8f82d3 100644 --- a/pkg/thumb/pipeline.go +++ b/pkg/thumb/pipeline.go @@ -4,14 +4,15 @@ import ( "context" "errors" "fmt" + "io" + "reflect" + "sort" + "github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource" "github.com/cloudreve/Cloudreve/v4/pkg/logging" "github.com/cloudreve/Cloudreve/v4/pkg/setting" "github.com/cloudreve/Cloudreve/v4/pkg/util" - "io" - "reflect" - "sort" ) type ( @@ -71,6 +72,7 @@ func NewPipeline(settings setting.Provider, l logging.Logger) Generator { NewVipsGenerator(l, settings), NewLibreOfficeGenerator(l, settings), NewMusicCoverGenerator(l, settings), + NewLibRawGenerator(l, settings), ) sort.Sort(generators) diff --git a/pkg/thumb/tester.go b/pkg/thumb/tester.go index 6439c6aa..e8f08c66 100644 --- a/pkg/thumb/tester.go +++ b/pkg/thumb/tester.go @@ -25,6 +25,8 @@ func TestGenerator(ctx context.Context, name, executable string) (string, error) return testLibreOfficeGenerator(ctx, executable) case "ffprobe": return testFFProbeGenerator(ctx, executable) + case "libraw": + return testLibRawGenerator(ctx, executable) default: return "", ErrUnknownGenerator } @@ -89,3 +91,20 @@ func testLibreOfficeGenerator(ctx context.Context, executable string) (string, e return output.String(), nil } + +func testLibRawGenerator(ctx context.Context, executable string) (string, error) { + cmd := exec.CommandContext(ctx, executable, "-L") + var output bytes.Buffer + cmd.Stdout = &output + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("failed to invoke libraw executable: %w", err) + } + + if !strings.Contains(output.String(), "Sony") { + return "", ErrUnknownOutput + } + + cameraList := strings.Split(output.String(), "\n") + + return fmt.Sprintf("N/A, %d cameras supported", len(cameraList)), nil +} From 2500ebc6a47201697196c5b9425d32d97ad6cdd8 Mon Sep 17 00:00:00 2001 From: WittF Date: Thu, 26 Jun 2025 14:58:58 +0800 Subject: [PATCH 10/27] refactor(captcha): update Cap to 2.0.0 (#2573) * refactor(captcha): update Cap backend to 2.0.0 API format * feat(captcha): add Cap version config for 1.x/2.x compatibility * fix(captcha): change Cap default version to 1.x for backward compatibility * refactor(captcha): remove Cap 1.x compatibility, keep only 2.x support * feat(captcha): update field names to Cap 2.0 standard - Site Key and Secret Key * fix(captcha): update Cap field names in defaults configuration --- inventory/setting.go | 4 ++-- middleware/captcha.go | 18 ++++++++++-------- pkg/setting/provider.go | 4 ++-- pkg/setting/types.go | 4 ++-- service/basic/site.go | 4 ++-- 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/inventory/setting.go b/inventory/setting.go index c08a2037..a07b35c7 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -393,8 +393,8 @@ var DefaultSettings = map[string]string{ "captcha_turnstile_site_key": "", "captcha_turnstile_site_secret": "", "captcha_cap_instance_url": "", - "captcha_cap_key_id": "", - "captcha_cap_key_secret": "", + "captcha_cap_site_key": "", + "captcha_cap_secret_key": "", "thumb_width": "400", "thumb_height": "300", "thumb_entity_suffix": "._thumb", diff --git a/middleware/captcha.go b/middleware/captcha.go index af72433a..9169d5f7 100644 --- a/middleware/captcha.go +++ b/middleware/captcha.go @@ -3,6 +3,12 @@ package middleware import ( "bytes" "encoding/json" + "io" + "net/http" + "net/url" + "strings" + "time" + "github.com/cloudreve/Cloudreve/v4/application/dependency" "github.com/cloudreve/Cloudreve/v4/pkg/logging" "github.com/cloudreve/Cloudreve/v4/pkg/recaptcha" @@ -11,11 +17,6 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/setting" "github.com/gin-gonic/gin" "github.com/mojocn/base64Captcha" - "io" - "net/http" - "net/url" - "strings" - "time" ) type req struct { @@ -133,7 +134,7 @@ func CaptchaRequired(enabled func(c *gin.Context) bool) gin.HandlerFunc { break case setting.CaptchaCap: captchaSetting := settings.CapCaptcha(c) - if captchaSetting.InstanceURL == "" || captchaSetting.KeyID == "" || captchaSetting.KeySecret == "" { + if captchaSetting.InstanceURL == "" || captchaSetting.SiteKey == "" || captchaSetting.SecretKey == "" { l.Warning("Cap verification failed: missing configuration") c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha configuration error", nil)) c.Abort() @@ -146,9 +147,10 @@ func CaptchaRequired(enabled func(c *gin.Context) bool) gin.HandlerFunc { request2.WithHeader(http.Header{"Content-Type": []string{"application/json"}}), ) - capEndpoint := strings.TrimSuffix(captchaSetting.InstanceURL, "/") + "/" + captchaSetting.KeyID + "/siteverify" + // Cap 2.0 API format: /{siteKey}/siteverify + capEndpoint := strings.TrimSuffix(captchaSetting.InstanceURL, "/") + "/" + captchaSetting.SiteKey + "/siteverify" requestBody := map[string]string{ - "secret": captchaSetting.KeySecret, + "secret": captchaSetting.SecretKey, "response": service.Ticket, } requestData, err := json.Marshal(requestBody) diff --git a/pkg/setting/provider.go b/pkg/setting/provider.go index 334d93d8..3d0b79a5 100644 --- a/pkg/setting/provider.go +++ b/pkg/setting/provider.go @@ -669,8 +669,8 @@ func (s *settingProvider) TurnstileCaptcha(ctx context.Context) *Turnstile { func (s *settingProvider) CapCaptcha(ctx context.Context) *Cap { return &Cap{ InstanceURL: s.getString(ctx, "captcha_cap_instance_url", ""), - KeyID: s.getString(ctx, "captcha_cap_key_id", ""), - KeySecret: s.getString(ctx, "captcha_cap_key_secret", ""), + SiteKey: s.getString(ctx, "captcha_cap_site_key", ""), + SecretKey: s.getString(ctx, "captcha_cap_secret_key", ""), } } diff --git a/pkg/setting/types.go b/pkg/setting/types.go index a31337a3..69ecfba2 100644 --- a/pkg/setting/types.go +++ b/pkg/setting/types.go @@ -50,8 +50,8 @@ type Turnstile struct { type Cap struct { InstanceURL string - KeyID string - KeySecret string + SiteKey string + SecretKey string } type SMTP struct { diff --git a/service/basic/site.go b/service/basic/site.go index 7a54d157..1f1ffe90 100644 --- a/service/basic/site.go +++ b/service/basic/site.go @@ -30,7 +30,7 @@ type SiteConfig struct { CaptchaType setting.CaptchaType `json:"captcha_type,omitempty"` TurnstileSiteID string `json:"turnstile_site_id,omitempty"` CapInstanceURL string `json:"captcha_cap_instance_url,omitempty"` - CapKeyID string `json:"captcha_cap_key_id,omitempty"` + CapSiteKey string `json:"captcha_cap_site_key,omitempty"` RegisterEnabled bool `json:"register_enabled,omitempty"` TosUrl string `json:"tos_url,omitempty"` PrivacyPolicyUrl string `json:"privacy_policy_url,omitempty"` @@ -137,7 +137,7 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) { TurnstileSiteID: settings.TurnstileCaptcha(c).Key, ReCaptchaKey: reCaptcha.Key, CapInstanceURL: capCaptcha.InstanceURL, - CapKeyID: capCaptcha.KeyID, + CapSiteKey: capCaptcha.SiteKey, AppPromotion: appSetting.Promotion, }, nil } From dc611bcb0d004764063c3cdbd2e9ba3914dad27c Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Thu, 26 Jun 2025 18:45:54 +0800 Subject: [PATCH 11/27] feat(explorer): manage created direct links / option to enable unique redirected direct links --- assets | 2 +- inventory/direct_link.go | 9 +++++++++ inventory/file.go | 18 ++++++++++-------- inventory/types/types.go | 1 + pkg/filemanager/fs/dbfs/dbfs.go | 9 +++++++++ pkg/filemanager/fs/fs.go | 1 + pkg/filemanager/manager/entity.go | 3 ++- routers/controllers/file.go | 10 ++++++++++ routers/router.go | 16 ++++++++++++---- service/explorer/file.go | 25 +++++++++++++++++++++++++ service/explorer/response.go | 25 +++++++++++++++++++++++-- 11 files changed, 103 insertions(+), 16 deletions(-) diff --git a/assets b/assets index 0fa57541..672b0019 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 0fa57541d5b1018251674ef01a7e799f6899564e +Subproject commit 672b00192cb89575b56805d338eb0fdc0ca7ed22 diff --git a/inventory/direct_link.go b/inventory/direct_link.go index dfed618b..f6bc356a 100644 --- a/inventory/direct_link.go +++ b/inventory/direct_link.go @@ -5,6 +5,7 @@ import ( "github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/ent/directlink" + "github.com/cloudreve/Cloudreve/v4/ent/schema" "github.com/cloudreve/Cloudreve/v4/pkg/conf" "github.com/cloudreve/Cloudreve/v4/pkg/hashid" ) @@ -16,6 +17,8 @@ type ( GetByNameID(ctx context.Context, id int, name string) (*ent.DirectLink, error) // GetByID get direct link by id GetByID(ctx context.Context, id int) (*ent.DirectLink, error) + // Delete delete direct link by id + Delete(ctx context.Context, id int) error } LoadDirectLinkFile struct{} ) @@ -60,6 +63,12 @@ func (d *directLinkClient) GetByNameID(ctx context.Context, id int, name string) return res, nil } +func (d *directLinkClient) Delete(ctx context.Context, id int) error { + ctx = schema.SkipSoftDelete(ctx) + _, err := d.client.DirectLink.Delete().Where(directlink.ID(id)).Exec(ctx) + return err +} + func withDirectLinkEagerLoading(ctx context.Context, q *ent.DirectLinkQuery) *ent.DirectLinkQuery { if v, ok := ctx.Value(LoadDirectLinkFile{}).(bool); ok && v { q.WithFile(func(m *ent.FileQuery) { diff --git a/inventory/file.go b/inventory/file.go index a99d40f9..b94d57f4 100644 --- a/inventory/file.go +++ b/inventory/file.go @@ -192,7 +192,7 @@ type FileClient interface { // UnlinkEntity unlinks an entity from a file UnlinkEntity(ctx context.Context, entity *ent.Entity, file *ent.File, owner *ent.User) (StorageDiff, error) // CreateDirectLink creates a direct link for a file - CreateDirectLink(ctx context.Context, fileID int, name string, speed int) (*ent.DirectLink, error) + CreateDirectLink(ctx context.Context, fileID int, name string, speed int, reuse bool) (*ent.DirectLink, error) // CountByTimeRange counts files created in a given time range CountByTimeRange(ctx context.Context, start, end *time.Time) (int, error) // CountEntityByTimeRange counts entities created in a given time range @@ -322,13 +322,15 @@ func (f *fileClient) CountEntityByStoragePolicyID(ctx context.Context, storagePo return v[0].Count, v[0].Sum, nil } -func (f *fileClient) CreateDirectLink(ctx context.Context, file int, name string, speed int) (*ent.DirectLink, error) { - // Find existed - existed, err := f.client.DirectLink. - Query(). - Where(directlink.FileID(file), directlink.Name(name), directlink.Speed(speed)).First(ctx) - if err == nil { - return existed, nil +func (f *fileClient) CreateDirectLink(ctx context.Context, file int, name string, speed int, reuse bool) (*ent.DirectLink, error) { + if reuse { + // Find existed + existed, err := f.client.DirectLink. + Query(). + Where(directlink.FileID(file), directlink.Name(name), directlink.Speed(speed)).First(ctx) + if err == nil { + return existed, nil + } } return f.client.DirectLink. diff --git a/inventory/types/types.go b/inventory/types/types.go index 2e6b9df8..95a3676b 100644 --- a/inventory/types/types.go +++ b/inventory/types/types.go @@ -206,6 +206,7 @@ const ( GroupPermission_CommunityPlaceholder4 GroupPermissionSetExplicitUser_placeholder GroupPermissionIgnoreFileOwnership // not used + GroupPermissionUniqueRedirectDirectLink ) const ( diff --git a/pkg/filemanager/fs/dbfs/dbfs.go b/pkg/filemanager/fs/dbfs/dbfs.go index cb7d3dc7..e12b24fa 100644 --- a/pkg/filemanager/fs/dbfs/dbfs.go +++ b/pkg/filemanager/fs/dbfs/dbfs.go @@ -384,6 +384,10 @@ func (f *DBFS) Get(ctx context.Context, path *fs.URI, opts ...fs.Option) (fs.Fil ctx = context.WithValue(ctx, inventory.LoadFileEntity{}, true) } + if o.extendedInfo { + ctx = context.WithValue(ctx, inventory.LoadFileDirectLink{}, true) + } + if o.loadFileShareIfOwned { ctx = context.WithValue(ctx, inventory.LoadFileShare{}, true) } @@ -407,6 +411,11 @@ func (f *DBFS) Get(ctx context.Context, path *fs.URI, opts ...fs.Option) (fs.Fil StorageUsed: target.SizeUsed(), EntityStoragePolicies: make(map[int]*ent.StoragePolicy), } + + if f.user.ID == target.OwnerID() { + extendedInfo.DirectLinks = target.Model.Edges.DirectLinks + } + policyID := target.PolicyID() if policyID > 0 { policy, err := f.storagePolicyClient.GetPolicyByID(ctx, policyID) diff --git a/pkg/filemanager/fs/fs.go b/pkg/filemanager/fs/fs.go index e6b1bde0..b081340c 100644 --- a/pkg/filemanager/fs/fs.go +++ b/pkg/filemanager/fs/fs.go @@ -191,6 +191,7 @@ type ( Shares []*ent.Share EntityStoragePolicies map[int]*ent.StoragePolicy View *types.ExplorerView + DirectLinks []*ent.DirectLink } FolderSummary struct { diff --git a/pkg/filemanager/manager/entity.go b/pkg/filemanager/manager/entity.go index 7198688c..a8954bca 100644 --- a/pkg/filemanager/manager/entity.go +++ b/pkg/filemanager/manager/entity.go @@ -98,8 +98,9 @@ func (m *manager) GetDirectLink(ctx context.Context, urls ...*fs.URI) ([]DirectL } if useRedirect { + reuseExisting := !m.user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionUniqueRedirectDirectLink)) // Use redirect source - link, err := fileClient.CreateDirectLink(ctx, file.ID(), file.Name(), m.user.Edges.Group.SpeedLimit) + link, err := fileClient.CreateDirectLink(ctx, file.ID(), file.Name(), m.user.Edges.Group.SpeedLimit, reuseExisting) if err != nil { ae.Add(url.String(), err) continue diff --git a/routers/controllers/file.go b/routers/controllers/file.go index 2f3d0490..dab661ad 100644 --- a/routers/controllers/file.go +++ b/routers/controllers/file.go @@ -116,6 +116,16 @@ func GetSource(c *gin.Context) { c.JSON(200, serializer.Response{Data: res}) } +func DeleteDirectLink(c *gin.Context) { + err := explorer.DeleteDirectLink(c) + if err != nil { + c.JSON(200, serializer.Err(c, err)) + return + } + + c.JSON(200, serializer.Response{}) +} + // Thumb 获取文件缩略图 func Thumb(c *gin.Context) { service := ParametersFromContext[*explorer.FileThumbService](c, explorer.FileThumbParameterCtx{}) diff --git a/routers/router.go b/routers/router.go index 3af6e4e9..af162238 100644 --- a/routers/router.go +++ b/routers/router.go @@ -697,10 +697,18 @@ func initMasterRouter(dep dependency.Dep) *gin.Engine { ) // 取得文件外链 - file.PUT("source", - controllers.FromJSON[explorer.GetDirectLinkService](explorer.GetDirectLinkParamCtx{}), - middleware.ValidateBatchFileCount(dep, explorer.GetDirectLinkParamCtx{}), - controllers.GetSource) + source := file.Group("source") + { + source.PUT("", + controllers.FromJSON[explorer.GetDirectLinkService](explorer.GetDirectLinkParamCtx{}), + middleware.ValidateBatchFileCount(dep, explorer.GetDirectLinkParamCtx{}), + controllers.GetSource, + ) + source.DELETE(":id", + middleware.HashID(hashid.SourceLinkID), + controllers.DeleteDirectLink, + ) + } // Patch view file.PATCH("view", controllers.FromJSON[explorer.PatchViewService](explorer.PatchViewParameterCtx{}), diff --git a/service/explorer/file.go b/service/explorer/file.go index 83aa8356..b2075828 100644 --- a/service/explorer/file.go +++ b/service/explorer/file.go @@ -120,6 +120,31 @@ func (s *GetDirectLinkService) Get(c *gin.Context) ([]DirectLinkResponse, error) return BuildDirectLinkResponse(res), err } +func DeleteDirectLink(c *gin.Context) error { + dep := dependency.FromContext(c) + user := inventory.UserFromContext(c) + m := manager.NewFileManager(dep, user) + defer m.Recycle() + + linkId := hashid.FromContext(c) + linkClient := dep.DirectLinkClient() + ctx := context.WithValue(c, inventory.LoadDirectLinkFile{}, true) + link, err := linkClient.GetByID(ctx, linkId) + if err != nil || link.Edges.File == nil { + return serializer.NewError(serializer.CodeNotFound, "Direct link not found", err) + } + + if link.Edges.File.OwnerID != user.ID { + return serializer.NewError(serializer.CodeNotFound, "Direct link not found", err) + } + + if err := linkClient.Delete(c, link.ID); err != nil { + return serializer.NewError(serializer.CodeDBError, "Failed to delete direct link", err) + } + + return nil +} + type ( // ListFileParameterCtx define key fore ListFileService ListFileParameterCtx struct{} diff --git a/service/explorer/response.go b/service/explorer/response.go index bcf0208c..b0251cb5 100644 --- a/service/explorer/response.go +++ b/service/explorer/response.go @@ -239,6 +239,14 @@ type ExtendedInfo struct { Shares []Share `json:"shares,omitempty"` Entities []Entity `json:"entities,omitempty"` View *types.ExplorerView `json:"view,omitempty"` + DirectLinks []DirectLink `json:"direct_links,omitempty"` +} + +type DirectLink struct { + ID string `json:"id"` + URL string `json:"url"` + Downloaded int `json:"downloaded"` + CreatedAt time.Time `json:"created_at"` } type StoragePolicy struct { @@ -372,16 +380,20 @@ func BuildExtendedInfo(ctx context.Context, u *ent.User, f fs.File, hasher hashi return nil } + dep := dependency.FromContext(ctx) + base := dep.SettingProvider().SiteURL(ctx) + ext := &ExtendedInfo{ StoragePolicy: BuildStoragePolicy(extendedInfo.StoragePolicy, hasher), StorageUsed: extendedInfo.StorageUsed, Entities: lo.Map(f.Entities(), func(e fs.Entity, index int) Entity { return BuildEntity(extendedInfo, e, hasher) }), + DirectLinks: lo.Map(extendedInfo.DirectLinks, func(d *ent.DirectLink, index int) DirectLink { + return BuildDirectLink(d, hasher, base) + }), } - dep := dependency.FromContext(ctx) - base := dep.SettingProvider().SiteURL(ctx) if u.ID == f.OwnerID() { // Only owner can see the shares settings. ext.Shares = lo.Map(extendedInfo.Shares, func(s *ent.Share, index int) Share { @@ -393,6 +405,15 @@ func BuildExtendedInfo(ctx context.Context, u *ent.User, f fs.File, hasher hashi return ext } +func BuildDirectLink(d *ent.DirectLink, hasher hashid.Encoder, base *url.URL) DirectLink { + return DirectLink{ + ID: hashid.EncodeSourceLinkID(hasher, d.ID), + URL: routes.MasterDirectLink(base, hashid.EncodeSourceLinkID(hasher, d.ID), d.Name).String(), + Downloaded: d.Downloads, + CreatedAt: d.CreatedAt, + } +} + func BuildEntity(extendedInfo *fs.FileExtendedInfo, e fs.Entity, hasher hashid.Encoder) Entity { var u *user.User createdBy := e.CreatedBy() From 4562042b8d86668d8211967ee642361b58c20bee Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Thu, 26 Jun 2025 18:48:07 +0800 Subject: [PATCH 12/27] fix(dashboard): cannot save settings for anonymous group --- inventory/group.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/inventory/group.go b/inventory/group.go index c40b6e89..3ecd697d 100644 --- a/inventory/group.go +++ b/inventory/group.go @@ -77,27 +77,34 @@ func (c *groupClient) ListAll(ctx context.Context) ([]*ent.Group, error) { } func (c *groupClient) Upsert(ctx context.Context, group *ent.Group) (*ent.Group, error) { - if group.ID == 0 { - return c.client.Group.Create(). + stm := c.client.Group.Create(). SetName(group.Name). SetMaxStorage(group.MaxStorage). SetSpeedLimit(group.SpeedLimit). SetPermissions(group.Permissions). - SetSettings(group.Settings). - SetStoragePoliciesID(group.Edges.StoragePolicies.ID). - Save(ctx) + SetSettings(group.Settings) + + if group.StoragePolicyID > 0 { + stm.SetStoragePolicyID(group.StoragePolicyID) + } + + return stm.Save(ctx) } - res, err := c.client.Group.UpdateOne(group). + stm := c.client.Group.UpdateOne(group). SetName(group.Name). SetMaxStorage(group.MaxStorage). SetSpeedLimit(group.SpeedLimit). SetPermissions(group.Permissions). SetSettings(group.Settings). - ClearStoragePolicies(). - SetStoragePoliciesID(group.Edges.StoragePolicies.ID). - Save(ctx) + ClearStoragePolicies() + + if group.StoragePolicyID > 0 { + stm.SetStoragePolicyID(group.StoragePolicyID) + } + + res, err := stm.Save(ctx) if err != nil { return nil, err } From 6c9a72af14e8a1d829ea65f36ecf6030e8afd8e4 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Thu, 26 Jun 2025 18:51:44 +0800 Subject: [PATCH 13/27] update submodule --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 672b0019..b483b3c5 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 672b00192cb89575b56805d338eb0fdc0ca7ed22 +Subproject commit b483b3c5ff4ccdff4b90e211023cf406bcc51482 From 02abeaed2e1830155ff3b07e29508d33d792aba9 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Fri, 27 Jun 2025 09:21:19 +0800 Subject: [PATCH 14/27] update submodule --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index b483b3c5..ac6f97d9 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit b483b3c5ff4ccdff4b90e211023cf406bcc51482 +Subproject commit ac6f97d9bab8b2da0ce91b1bac30d8f83f6359ab From d382bd8f8d799dfca3136ef42075ef25c32259b4 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Fri, 27 Jun 2025 12:53:07 +0800 Subject: [PATCH 15/27] fix(dashboard): cannot change storage policy for groups (#2577) --- inventory/group.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inventory/group.go b/inventory/group.go index 3ecd697d..70089edf 100644 --- a/inventory/group.go +++ b/inventory/group.go @@ -100,8 +100,8 @@ func (c *groupClient) Upsert(ctx context.Context, group *ent.Group) (*ent.Group, SetSettings(group.Settings). ClearStoragePolicies() - if group.StoragePolicyID > 0 { - stm.SetStoragePolicyID(group.StoragePolicyID) + if group.Edges.StoragePolicies != nil && group.Edges.StoragePolicies.ID > 0 { + stm.SetStoragePolicyID(group.Edges.StoragePolicies.ID) } res, err := stm.Save(ctx) From f38f32f9f58136bc32dc38f54c107188ed7c299b Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Fri, 27 Jun 2025 13:54:10 +0800 Subject: [PATCH 16/27] fix(db): sslmode prefer not supported in some pg version (?) related: #2540 --- inventory/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventory/client.go b/inventory/client.go index 52420538..bbbee4b9 100644 --- a/inventory/client.go +++ b/inventory/client.go @@ -75,7 +75,7 @@ func NewRawEntClient(l logging.Logger, config conf.ConfigProvider) (*ent.Client, client, err = sql.Open("sqlite3", util.RelativePath(dbConfig.DBFile)) case conf.PostgresDB: l.Info("Connect to Postgres database %q.", dbConfig.Host) - client, err = sql.Open("postgres", fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=allow", + client, err = sql.Open("postgres", fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable", dbConfig.Host, dbConfig.User, dbConfig.Password, From 6106b57bc7c7abe2bc3950f380fa114d876c89cc Mon Sep 17 00:00:00 2001 From: WittF Date: Sun, 29 Jun 2025 10:14:26 +0800 Subject: [PATCH 17/27] feat(captcha): update static asset source option (#2589) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(captcha): Add captcha_cap_asset_server configuration option to support static asset server settings (#2584) * fix(captcha): Backend default: cdn → jsdelivr --- inventory/setting.go | 1 + pkg/setting/provider.go | 1 + pkg/setting/types.go | 1 + service/basic/site.go | 2 ++ 4 files changed, 5 insertions(+) diff --git a/inventory/setting.go b/inventory/setting.go index a07b35c7..d8dd0b59 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -395,6 +395,7 @@ var DefaultSettings = map[string]string{ "captcha_cap_instance_url": "", "captcha_cap_site_key": "", "captcha_cap_secret_key": "", + "captcha_cap_asset_server": "jsdelivr", "thumb_width": "400", "thumb_height": "300", "thumb_entity_suffix": "._thumb", diff --git a/pkg/setting/provider.go b/pkg/setting/provider.go index 3d0b79a5..09aacbea 100644 --- a/pkg/setting/provider.go +++ b/pkg/setting/provider.go @@ -671,6 +671,7 @@ func (s *settingProvider) CapCaptcha(ctx context.Context) *Cap { InstanceURL: s.getString(ctx, "captcha_cap_instance_url", ""), SiteKey: s.getString(ctx, "captcha_cap_site_key", ""), SecretKey: s.getString(ctx, "captcha_cap_secret_key", ""), + AssetServer: s.getString(ctx, "captcha_cap_asset_server", "jsdelivr"), } } diff --git a/pkg/setting/types.go b/pkg/setting/types.go index 69ecfba2..6c389c23 100644 --- a/pkg/setting/types.go +++ b/pkg/setting/types.go @@ -52,6 +52,7 @@ type Cap struct { InstanceURL string SiteKey string SecretKey string + AssetServer string } type SMTP struct { diff --git a/service/basic/site.go b/service/basic/site.go index 1f1ffe90..aface24c 100644 --- a/service/basic/site.go +++ b/service/basic/site.go @@ -31,6 +31,7 @@ type SiteConfig struct { TurnstileSiteID string `json:"turnstile_site_id,omitempty"` CapInstanceURL string `json:"captcha_cap_instance_url,omitempty"` CapSiteKey string `json:"captcha_cap_site_key,omitempty"` + CapAssetServer string `json:"captcha_cap_asset_server,omitempty"` RegisterEnabled bool `json:"register_enabled,omitempty"` TosUrl string `json:"tos_url,omitempty"` PrivacyPolicyUrl string `json:"privacy_policy_url,omitempty"` @@ -138,6 +139,7 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) { ReCaptchaKey: reCaptcha.Key, CapInstanceURL: capCaptcha.InstanceURL, CapSiteKey: capCaptcha.SiteKey, + CapAssetServer: capCaptcha.AssetServer, AppPromotion: appSetting.Promotion, }, nil } From 642c32c6cc64bbf7bb02dc60e428dfafe05a1fff Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Sun, 29 Jun 2025 10:47:59 +0800 Subject: [PATCH 18/27] chore: update fatih/color (#2591) --- assets | 2 +- go.mod | 4 ++-- go.sum | 6 ++++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/assets b/assets index ac6f97d9..e19056c7 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit ac6f97d9bab8b2da0ce91b1bac30d8f83f6359ab +Subproject commit e19056c746b4a2ccc3b7aef72f97402c7f8fa7b0 diff --git a/go.mod b/go.mod index 1fb75381..895da74e 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d github.com/dsoprea/go-tiff-image-structure v0.0.0-20221003165014-8ecc4f52edca github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf - github.com/fatih/color v1.9.0 + github.com/fatih/color v1.18.0 github.com/gin-contrib/cors v1.3.0 github.com/gin-contrib/sessions v1.0.2 github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2 @@ -110,7 +110,7 @@ require ( github.com/klauspost/pgzip v1.2.5 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/mattn/go-colorable v0.1.6 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect diff --git a/go.sum b/go.sum index 8fe73300..6020f0e7 100644 --- a/go.sum +++ b/go.sum @@ -276,6 +276,8 @@ github.com/etcd-io/gofail v0.0.0-20190801230047-ad7f989257ca/go.mod h1:49H/RkXP8 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= @@ -660,6 +662,8 @@ github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -669,6 +673,7 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= @@ -1248,6 +1253,7 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From e0b2b4649e167350064ff5a8c5e37bb945763981 Mon Sep 17 00:00:00 2001 From: Anye <53684988+Anyexyz@users.noreply.github.com> Date: Mon, 30 Jun 2025 19:32:21 +0800 Subject: [PATCH 19/27] fix(db): map MariaDB type to MySQL (#2587) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(db): 将MariaDB数据库类型映射到MySQL类型 * Update client.go --- inventory/client.go | 3 +++ pkg/conf/types.go | 1 + 2 files changed, 4 insertions(+) diff --git a/inventory/client.go b/inventory/client.go index bbbee4b9..7f342abb 100644 --- a/inventory/client.go +++ b/inventory/client.go @@ -52,6 +52,9 @@ func NewRawEntClient(l logging.Logger, config conf.ConfigProvider) (*ent.Client, if confDBType == conf.SQLite3DB || confDBType == "" { confDBType = conf.SQLiteDB } + if confDBType == conf.MariaDB { + confDBType = conf.MySqlDB + } var ( err error diff --git a/pkg/conf/types.go b/pkg/conf/types.go index 285278fb..e5a53b4d 100644 --- a/pkg/conf/types.go +++ b/pkg/conf/types.go @@ -10,6 +10,7 @@ var ( MySqlDB DBType = "mysql" MsSqlDB DBType = "mssql" PostgresDB DBType = "postgres" + MariaDB DBType = "mariadb" ) // Database 数据库 From 19a65b065c5ab28f8432fce702dbf94d4a076931 Mon Sep 17 00:00:00 2001 From: Samler Date: Mon, 30 Jun 2025 19:34:18 +0800 Subject: [PATCH 20/27] fix: new user group error in without replication (#2596) --- inventory/group.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inventory/group.go b/inventory/group.go index 70089edf..9c8f8fac 100644 --- a/inventory/group.go +++ b/inventory/group.go @@ -85,8 +85,8 @@ func (c *groupClient) Upsert(ctx context.Context, group *ent.Group) (*ent.Group, SetPermissions(group.Permissions). SetSettings(group.Settings) - if group.StoragePolicyID > 0 { - stm.SetStoragePolicyID(group.StoragePolicyID) + if group.Edges.StoragePolicies != nil && group.Edges.StoragePolicies.ID > 0 { + stm.SetStoragePolicyID(group.Edges.StoragePolicies.ID) } return stm.Save(ctx) From 17fc598fb3a76ca3914c5c0a62f1dad5f1d52a53 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Mon, 30 Jun 2025 19:46:22 +0800 Subject: [PATCH 21/27] doc: duplicated OneDrive in README --- README.md | 2 +- assets | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9f8b2be6..5b7e67bd 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ ## :sparkles: Features -- :cloud: Support storing files into Local, Remote node, OneDrive, S3 compatible API, Qiniu, Aliyun OSS, Tencent COS, Upyun, OneDrive. +- :cloud: Support storing files into Local, Remote node, OneDrive, S3 compatible API, Qiniu, Aliyun OSS, Tencent COS, Upyun. - :outbox_tray: Upload/Download in directly transmission from client to storage providers. - 💾 Integrate with Aria2/qBittorrent to download files in background, use multiple download nodes to share the load. - 📚 Compress/Extract files, download files in batch. diff --git a/assets b/assets index e19056c7..8e2c2bcf 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit e19056c746b4a2ccc3b7aef72f97402c7f8fa7b0 +Subproject commit 8e2c2bcff17d4728a01c2cabab8c3b639d72f428 From a0aefef691fc494a516d404a42cb10ef50198eac Mon Sep 17 00:00:00 2001 From: Samler Date: Thu, 3 Jul 2025 14:04:14 +0800 Subject: [PATCH 22/27] feat: platform self-adaptation for file viewer application (#2603) --- inventory/types/types.go | 1 + 1 file changed, 1 insertion(+) diff --git a/inventory/types/types.go b/inventory/types/types.go index 95a3676b..b3874257 100644 --- a/inventory/types/types.go +++ b/inventory/types/types.go @@ -286,6 +286,7 @@ type Viewer struct { MaxSize int64 `json:"max_size,omitempty"` Disabled bool `json:"disabled,omitempty"` Templates []NewFileTemplate `json:"templates,omitempty"` + Platform string `json:"platform,omitempty"` } type ViewerGroup struct { From aada3aab0289228af20a36ab962e33a90bd00f64 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Fri, 4 Jul 2025 10:05:15 +0800 Subject: [PATCH 23/27] feat(storage): load balance storage policy (#2436) --- assets | 2 +- inventory/policy.go | 9 +++++++++ service/admin/policy.go | 13 ++++++++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/assets b/assets index 8e2c2bcf..27996dc3 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 8e2c2bcff17d4728a01c2cabab8c3b639d72f428 +Subproject commit 27996dc3ea22ab3b7ae525841ad1d45098d375f4 diff --git a/inventory/policy.go b/inventory/policy.go index 2343cf6e..285c01b1 100644 --- a/inventory/policy.go +++ b/inventory/policy.go @@ -27,6 +27,7 @@ type ( SkipStoragePolicyCache struct{} StoragePolicyClient interface { + TxOperator // GetByGroup returns the storage policies of the group. GetByGroup(ctx context.Context, group *ent.Group) (*ent.StoragePolicy, error) // GetPolicyByID returns the storage policy by id. @@ -64,6 +65,14 @@ type storagePolicyClient struct { cache cache.Driver } +func (c *storagePolicyClient) SetClient(newClient *ent.Client) TxOperator { + return &storagePolicyClient{client: newClient, cache: c.cache} +} + +func (c *storagePolicyClient) GetClient() *ent.Client { + return c.client +} + func (c *storagePolicyClient) Delete(ctx context.Context, policy *ent.StoragePolicy) error { if err := c.client.StoragePolicy.DeleteOne(policy).Exec(ctx); err != nil { return fmt.Errorf("failed to delete storage policy: %w", err) diff --git a/service/admin/policy.go b/service/admin/policy.go index e73e5cd6..aa6625cf 100644 --- a/service/admin/policy.go +++ b/service/admin/policy.go @@ -294,11 +294,22 @@ func (service *UpdateStoragePolicyService) Update(c *gin.Context) (*GetStoragePo } service.Policy.ID = idInt - _, err = storagePolicyClient.Upsert(c, service.Policy) + + sc, tx, ctx, err := inventory.WithTx(c, storagePolicyClient) + if err != nil { + return nil, serializer.NewError(serializer.CodeDBError, "Failed to create transaction", err) + } + + _, err = sc.Upsert(ctx, service.Policy) if err != nil { + _ = inventory.Rollback(tx) return nil, serializer.NewError(serializer.CodeDBError, "Failed to update policy", err) } + if err := inventory.Commit(tx); err != nil { + return nil, serializer.NewError(serializer.CodeDBError, "Failed to commit transaction", err) + } + _ = dep.KV().Delete(manager.EntityUrlCacheKeyPrefix) s := SingleStoragePolicyService{ID: idInt} From fe2ccb4d4e26114c166a36088f2939d144ba76c4 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Fri, 4 Jul 2025 14:40:32 +0800 Subject: [PATCH 24/27] feat(share): add option to automatically render and show README file (#2382) --- assets | 2 +- inventory/types/types.go | 2 ++ pkg/filemanager/manager/manager.go | 1 + pkg/filemanager/manager/operation.go | 3 ++- service/explorer/response.go | 2 ++ service/share/manage.go | 2 ++ 6 files changed, 10 insertions(+), 2 deletions(-) diff --git a/assets b/assets index 27996dc3..38f51144 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 27996dc3ea22ab3b7ae525841ad1d45098d375f4 +Subproject commit 38f5114426a43840e4e86e71f0d6d369e2adb7c7 diff --git a/inventory/types/types.go b/inventory/types/types.go index b3874257..44103e4a 100644 --- a/inventory/types/types.go +++ b/inventory/types/types.go @@ -177,6 +177,8 @@ type ( ShareProps struct { // Whether to share view setting from owner ShareView bool `json:"share_view,omitempty"` + // Whether to automatically show readme file in share view + ShowReadMe bool `json:"show_read_me,omitempty"` } FileTypeIconSetting struct { diff --git a/pkg/filemanager/manager/manager.go b/pkg/filemanager/manager/manager.go index 6cce600c..ad04af5e 100644 --- a/pkg/filemanager/manager/manager.go +++ b/pkg/filemanager/manager/manager.go @@ -115,6 +115,7 @@ type ( RemainDownloads int Expire *time.Time ShareView bool + ShowReadMe bool } ) diff --git a/pkg/filemanager/manager/operation.go b/pkg/filemanager/manager/operation.go index fe97b4cc..9ba859bc 100644 --- a/pkg/filemanager/manager/operation.go +++ b/pkg/filemanager/manager/operation.go @@ -267,7 +267,8 @@ func (l *manager) CreateOrUpdateShare(ctx context.Context, path *fs.URI, args *C } props := &types.ShareProps{ - ShareView: args.ShareView, + ShareView: args.ShareView, + ShowReadMe: args.ShowReadMe, } share, err := shareClient.Upsert(ctx, &inventory.CreateShareParams{ diff --git a/service/explorer/response.go b/service/explorer/response.go index b0251cb5..10afdb05 100644 --- a/service/explorer/response.go +++ b/service/explorer/response.go @@ -280,6 +280,7 @@ type Share struct { CreatedAt time.Time `json:"created_at,omitempty"` Expired bool `json:"expired"` Url string `json:"url"` + ShowReadMe bool `json:"show_readme,omitempty"` // Only viewable by owner IsPrivate bool `json:"is_private,omitempty"` @@ -313,6 +314,7 @@ func BuildShare(s *ent.Share, base *url.URL, hasher hashid.Encoder, requester *e res.Downloaded = s.Downloads res.Expires = s.Expires res.Password = s.Password + res.ShowReadMe = s.Props != nil && s.Props.ShowReadMe } if requester.ID == owner.ID { diff --git a/service/share/manage.go b/service/share/manage.go index 90e773a0..335f3468 100644 --- a/service/share/manage.go +++ b/service/share/manage.go @@ -24,6 +24,7 @@ type ( RemainDownloads int `json:"downloads"` Expire int `json:"expire"` ShareView bool `json:"share_view"` + ShowReadMe bool `json:"show_readme"` } ShareCreateParamCtx struct{} ) @@ -58,6 +59,7 @@ func (service *ShareCreateService) Upsert(c *gin.Context, existed int) (string, Expire: expires, ExistedShareID: existed, ShareView: service.ShareView, + ShowReadMe: service.ShowReadMe, }) if err != nil { return "", err From 75a03aa708a900032479de5fea56eb3075913b7e Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Sat, 5 Jul 2025 10:05:09 +0800 Subject: [PATCH 25/27] fix(auth): unified empty path for sign content (#2616) --- pkg/auth/auth.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index cbc66418..ccc8de78 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -220,5 +220,8 @@ func getUrlSignContent(ctx context.Context, url *url.URL) string { // host = strings.TrimSuffix(host, "/") // // remove port if it exists // host = strings.Split(host, ":")[0] + if url.Path == "" { + return "/" + } return url.Path } From 617d3a4262b8c0c615f3d27b4891805dd942d6b3 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Sat, 5 Jul 2025 10:50:51 +0800 Subject: [PATCH 26/27] feat(qiniu): use accelerated upload domain (#2497) --- assets | 2 +- inventory/types/types.go | 2 ++ pkg/filemanager/driver/qiniu/qiniu.go | 5 ++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/assets b/assets index 38f51144..6a6fd722 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 38f5114426a43840e4e86e71f0d6d369e2adb7c7 +Subproject commit 6a6fd722f35d3fca9eb1c34297e3f3eae8c9a161 diff --git a/inventory/types/types.go b/inventory/types/types.go index 44103e4a..8bc383a8 100644 --- a/inventory/types/types.go +++ b/inventory/types/types.go @@ -90,6 +90,8 @@ type ( UseCname bool `json:"use_cname,omitempty"` // CDN domain does not need to be signed. SourceAuth bool `json:"source_auth,omitempty"` + // QiniuUploadCdn whether to use CDN for Qiniu upload. + QiniuUploadCdn bool `json:"qiniu_upload_cdn,omitempty"` } FileType int diff --git a/pkg/filemanager/driver/qiniu/qiniu.go b/pkg/filemanager/driver/qiniu/qiniu.go index 3df57989..d6dad012 100644 --- a/pkg/filemanager/driver/qiniu/qiniu.go +++ b/pkg/filemanager/driver/qiniu/qiniu.go @@ -67,7 +67,10 @@ func New(ctx context.Context, policy *ent.StoragePolicy, settings setting.Provid } mac := qbox.NewMac(policy.AccessKey, policy.SecretKey) - cfg := &storage.Config{UseHTTPS: true} + cfg := &storage.Config{ + UseHTTPS: true, + UseCdnDomains: policy.Settings.QiniuUploadCdn, + } driver := &Driver{ policy: policy, From b13490357bc3ea3a570c9a68a8fb74d3865e7a2d Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Sat, 5 Jul 2025 11:52:15 +0800 Subject: [PATCH 27/27] feat(dashboard): cleanup tasks and events (#2368) --- assets | 2 +- inventory/task.go | 26 ++++++++++++++++++++++++++ routers/controllers/admin.go | 11 +++++++++++ routers/router.go | 5 +++++ service/admin/task.go | 29 +++++++++++++++++++++++++++++ 5 files changed, 72 insertions(+), 1 deletion(-) diff --git a/assets b/assets index 6a6fd722..e9b91c4e 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 6a6fd722f35d3fca9eb1c34297e3f3eae8c9a161 +Subproject commit e9b91c4e03654d5968f8a676a13fc4badf530b5d diff --git a/inventory/task.go b/inventory/task.go index 5e64e614..81edf774 100644 --- a/inventory/task.go +++ b/inventory/task.go @@ -3,6 +3,7 @@ package inventory import ( "context" "fmt" + "time" "entgo.io/ent/dialect/sql" "github.com/cloudreve/Cloudreve/v4/ent" @@ -44,6 +45,8 @@ type TaskClient interface { List(ctx context.Context, args *ListTaskArgs) (*ListTaskResult, error) // DeleteByIDs deletes the tasks with the given IDs. DeleteByIDs(ctx context.Context, ids ...int) error + // DeleteBy deletes the tasks with the given args. + DeleteBy(ctx context.Context, args *DeleteTaskArgs) error } type ( @@ -59,6 +62,12 @@ type ( *PaginationResults Tasks []*ent.Task } + + DeleteTaskArgs struct { + NotAfter time.Time + Types []string + Status []task.Status + } ) func NewTaskClient(client *ent.Client, dbType conf.DBType, hasher hashid.Encoder) TaskClient { @@ -113,6 +122,23 @@ func (c *taskClient) DeleteByIDs(ctx context.Context, ids ...int) error { return err } +func (c *taskClient) DeleteBy(ctx context.Context, args *DeleteTaskArgs) error { + query := c.client.Task. + Delete(). + Where(task.CreatedAtLTE(args.NotAfter)) + + if len(args.Status) > 0 { + query.Where(task.StatusIn(args.Status...)) + } + + if len(args.Types) > 0 { + query.Where(task.TypeIn(args.Types...)) + } + + _, err := query.Exec(ctx) + return err +} + func (c *taskClient) Update(ctx context.Context, task *ent.Task, args *TaskArgs) (*ent.Task, error) { stm := c.client.Task.UpdateOne(task). SetPublicState(args.PublicState) diff --git a/routers/controllers/admin.go b/routers/controllers/admin.go index 2be65b0a..12e7ab50 100644 --- a/routers/controllers/admin.go +++ b/routers/controllers/admin.go @@ -518,6 +518,17 @@ func AdminBatchDeleteEntity(c *gin.Context) { } } +func AdminCleanupTask(c *gin.Context) { + service := ParametersFromContext[*admin.CleanupTaskService](c, admin.CleanupTaskParameterCtx{}) + err := service.CleanupTask(c) + if err != nil { + c.JSON(200, serializer.Err(c, err)) + return + } + + c.JSON(200, serializer.Response{}) +} + func AdminListTasks(c *gin.Context) { service := ParametersFromContext[*admin.AdminListService](c, admin.AdminListServiceParamsCtx{}) res, err := service.Tasks(c) diff --git a/routers/router.go b/routers/router.go index af162238..f5897207 100644 --- a/routers/router.go +++ b/routers/router.go @@ -854,6 +854,11 @@ func initMasterRouter(dep dependency.Dep) *gin.Engine { controllers.FromJSON[adminsvc.BatchTaskService](adminsvc.BatchTaskParamCtx{}), controllers.AdminBatchDeleteTask, ) + // Cleanup tasks + queue.POST("cleanup", + controllers.FromJSON[adminsvc.CleanupTaskService](adminsvc.CleanupTaskParameterCtx{}), + controllers.AdminCleanupTask, + ) // // 列出任务 // queue.POST("list", controllers.AdminListTask) // // 新建文件导入任务 diff --git a/service/admin/task.go b/service/admin/task.go index 610da415..c6811bcb 100644 --- a/service/admin/task.go +++ b/service/admin/task.go @@ -3,6 +3,7 @@ package admin import ( "context" "strconv" + "time" "github.com/cloudreve/Cloudreve/v4/application/dependency" "github.com/cloudreve/Cloudreve/v4/ent" @@ -251,3 +252,31 @@ func (s *BatchTaskService) Delete(c *gin.Context) error { return nil } + +type ( + CleanupTaskService struct { + NotAfter time.Time `json:"not_after" binding:"required"` + Types []string `json:"types"` + Status []task.Status `json:"status"` + } + CleanupTaskParameterCtx struct{} +) + +func (s *CleanupTaskService) CleanupTask(c *gin.Context) error { + dep := dependency.FromContext(c) + taskClient := dep.TaskClient() + + if len(s.Status) == 0 { + s.Status = []task.Status{task.StatusCanceled, task.StatusCompleted, task.StatusError} + } + + if err := taskClient.DeleteBy(c, &inventory.DeleteTaskArgs{ + NotAfter: s.NotAfter, + Types: s.Types, + Status: s.Status, + }); err != nil { + return serializer.NewError(serializer.CodeDBError, "Failed to cleanup tasks", err) + } + + return nil +}