init
This commit is contained in:
		
							
								
								
									
										186
									
								
								models/defaults.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								models/defaults.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,186 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/cache"
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/conf"
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/util"
 | 
			
		||||
	"github.com/gofrs/uuid"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var defaultSettings = []Setting{
 | 
			
		||||
	{Name: "siteURL", Value: `http://localhost`, Type: "basic"},
 | 
			
		||||
	{Name: "siteName", Value: `CloudrevePlus`, Type: "basic"},
 | 
			
		||||
	{Name: "register_enabled", Value: `1`, Type: "register"},
 | 
			
		||||
	{Name: "default_group", Value: `2`, Type: "register"},
 | 
			
		||||
	{Name: "mail_domain_filter", Value: `0`, Type: "register"},
 | 
			
		||||
	{Name: "mail_domain_filter_list", Value: `126.com,163.com,gmail.com,outlook.com,qq.com,foxmail.com,yeah.net,sohu.com,sohu.cn,139.com,wo.cn,189.cn,hotmail.com,live.com,live.cn`, Type: "register"},
 | 
			
		||||
	{Name: "siteKeywords", Value: `CloudrevePlus, cloud storage`, Type: "basic"},
 | 
			
		||||
	{Name: "siteDes", Value: `部署公私兼备的网盘系统`, Type: "basic"},
 | 
			
		||||
	{Name: "siteTitle", Value: `Inclusive cloud storage for everyone`, Type: "basic"},
 | 
			
		||||
	{Name: "siteNotice", Value: ``, Type: "basic"},
 | 
			
		||||
	{Name: "siteScript", Value: ``, Type: "basic"},
 | 
			
		||||
	{Name: "siteID", Value: uuid.Must(uuid.NewV4()).String(), Type: "basic"},
 | 
			
		||||
	{Name: "fromName", Value: `Cloudreve`, Type: "mail"},
 | 
			
		||||
	{Name: "mail_keepalive", Value: `30`, Type: "mail"},
 | 
			
		||||
	{Name: "fromAdress", Value: `no-reply@acg.blue`, Type: "mail"},
 | 
			
		||||
	{Name: "smtpHost", Value: `smtp.mxhichina.com`, Type: "mail"},
 | 
			
		||||
	{Name: "smtpPort", Value: `25`, Type: "mail"},
 | 
			
		||||
	{Name: "replyTo", Value: `abslant@126.com`, Type: "mail"},
 | 
			
		||||
	{Name: "smtpUser", Value: `no-reply@acg.blue`, Type: "mail"},
 | 
			
		||||
	{Name: "smtpPass", Value: ``, Type: "mail"},
 | 
			
		||||
	{Name: "smtpEncryption", Value: `0`, Type: "mail"},
 | 
			
		||||
	{Name: "over_used_template", Value: `<meta name="viewport"content="width=device-width"><meta http-equiv="Content-Type"content="text/html; charset=UTF-8"><title>容量超额提醒</title><style type="text/css">img{max-width:100%}body{-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:none;width:100%!important;height:100%;line-height:1.6em}body{background-color:#f6f6f6}@media only screen and(max-width:640px){body{padding:0!important}h1{font-weight:800!important;margin:20px 0 5px!important}h2{font-weight:800!important;margin:20px 0 5px!important}h3{font-weight:800!important;margin:20px 0 5px!important}h4{font-weight:800!important;margin:20px 0 5px!important}h1{font-size:22px!important}h2{font-size:18px!important}h3{font-size:16px!important}.container{padding:0!important;width:100%!important}.content{padding:0!important}.content-wrap{padding:10px!important}.invoice{width:100%!important}}</style><table class="body-wrap"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;"bgcolor="#f6f6f6"><tbody><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;"valign="top"></td><td class="container"width="600"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;"valign="top"><div class="content"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;"><table class="main"width="100%"cellpadding="0"cellspacing="0"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px
 | 
			
		||||
solid #e9e9e9;"bgcolor="#fff"><tbody><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="alert alert-warning"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 16px; vertical-align: top; color: #fff; font-weight: 500; text-align: center; border-radius: 3px 3px 0 0; background-color: #FF9F00; margin: 0; padding: 20px;"align="center"bgcolor="#FF9F00"valign="top">容量超额警告</td></tr><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-wrap"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;"valign="top"><table width="100%"cellpadding="0"cellspacing="0"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><tbody><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-block"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;"valign="top">亲爱的<strong style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">{userName}</strong>:</td></tr><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-block"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;"valign="top">由于{notifyReason},您在{siteTitle}的账户的容量使用超出配额,您将无法继续上传新文件,请尽快清理文件,否则我们将会禁用您的账户。</td></tr><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-block"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;"valign="top"><a href="{siteUrl}Login"class="btn-primary"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;">登录{siteTitle}</a></td></tr><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-block"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;"valign="top">感谢您选择{siteTitle}。</td></tr></tbody></table></td></tr></tbody></table><div class="footer"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;"><table width="100%"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><tbody><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="aligncenter content-block"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;"align="center"valign="top">此邮件由系统自动发送,请不要直接回复。</td></tr></tbody></table></div></div></td><td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;"valign="top"></td></tr></tbody></table>`, Type: "mail_template"},
 | 
			
		||||
	{Name: "ban_time", Value: `604800`, Type: "storage_policy"},
 | 
			
		||||
	{Name: "maxEditSize", Value: `52428800`, Type: "file_edit"},
 | 
			
		||||
	{Name: "archive_timeout", Value: `600`, Type: "timeout"},
 | 
			
		||||
	{Name: "download_timeout", Value: `600`, Type: "timeout"},
 | 
			
		||||
	{Name: "preview_timeout", Value: `600`, Type: "timeout"},
 | 
			
		||||
	{Name: "doc_preview_timeout", Value: `600`, Type: "timeout"},
 | 
			
		||||
	{Name: "upload_session_timeout", Value: `86400`, Type: "timeout"},
 | 
			
		||||
	{Name: "slave_api_timeout", Value: `60`, Type: "timeout"},
 | 
			
		||||
	{Name: "slave_node_retry", Value: `3`, Type: "slave"},
 | 
			
		||||
	{Name: "slave_ping_interval", Value: `60`, Type: "slave"},
 | 
			
		||||
	{Name: "slave_recover_interval", Value: `120`, Type: "slave"},
 | 
			
		||||
	{Name: "slave_transfer_timeout", Value: `172800`, Type: "timeout"},
 | 
			
		||||
	{Name: "onedrive_monitor_timeout", Value: `600`, Type: "timeout"},
 | 
			
		||||
	{Name: "onedrive_source_timeout", Value: `1800`, Type: "timeout"},
 | 
			
		||||
	{Name: "share_download_session_timeout", Value: `2073600`, Type: "timeout"},
 | 
			
		||||
	{Name: "onedrive_callback_check", Value: `20`, Type: "timeout"},
 | 
			
		||||
	{Name: "folder_props_timeout", Value: `300`, Type: "timeout"},
 | 
			
		||||
	{Name: "chunk_retries", Value: `5`, Type: "retry"},
 | 
			
		||||
	{Name: "onedrive_source_timeout", Value: `1800`, Type: "timeout"},
 | 
			
		||||
	{Name: "reset_after_upload_failed", Value: `0`, Type: "upload"},
 | 
			
		||||
	{Name: "use_temp_chunk_buffer", Value: `1`, Type: "upload"},
 | 
			
		||||
	{Name: "login_captcha", Value: `0`, Type: "login"},
 | 
			
		||||
	{Name: "qq_login", Value: `0`, Type: "login"},
 | 
			
		||||
	{Name: "qq_direct_login", Value: `0`, Type: "login"},
 | 
			
		||||
	{Name: "qq_login_id", Value: ``, Type: "login"},
 | 
			
		||||
	{Name: "qq_login_key", Value: ``, Type: "login"},
 | 
			
		||||
	{Name: "reg_captcha", Value: `0`, Type: "login"},
 | 
			
		||||
	{Name: "email_active", Value: `0`, Type: "register"},
 | 
			
		||||
	{Name: "mail_activation_template", Value: `<!DOCTYPE html PUBLIC"-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box;
 | 
			
		||||
font-size: 14px; margin: 0;"><head><meta name="viewport"content="width=device-width"/><meta http-equiv="Content-Type"content="text/html; charset=UTF-8"/><title>用户激活</title><style type="text/css">img{max-width:100%}body{-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:none;width:100%!important;height:100%;line-height:1.6em}body{background-color:#f6f6f6}@media only screen and(max-width:640px){body{padding:0!important}h1{font-weight:800!important;margin:20px 0 5px!important}h2{font-weight:800!important;margin:20px 0 5px!important}h3{font-weight:800!important;margin:20px 0 5px!important}h4{font-weight:800!important;margin:20px 0 5px!important}h1{font-size:22px!important}h2{font-size:18px!important}h3{font-size:16px!important}.container{padding:0!important;width:100%!important}.content{padding:0!important}.content-wrap{padding:10px!important}.invoice{width:100%!important}}</style></head><body itemscope itemtype="http://schema.org/EmailMessage"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing:
 | 
			
		||||
border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;"bgcolor="#f6f6f6"><table class="body-wrap"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;"bgcolor="#f6f6f6"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif;
 | 
			
		||||
box-sizing: border-box; font-size: 14px; margin: 0;"><td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;"valign="top"></td><td class="container"width="600"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;"valign="top"><div class="content"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;"><table class="main"width="100%"cellpadding="0"cellspacing="0"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px
 | 
			
		||||
solid #e9e9e9;"bgcolor="#fff"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size:
 | 
			
		||||
14px; margin: 0;"><td class="alert alert-warning"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 16px; vertical-align: top; color: #fff; font-weight: 500; text-align: center; border-radius: 3px 3px 0 0; background-color: #009688; margin: 0; padding: 20px;"align="center"bgcolor="#FF9F00"valign="top">激活{siteTitle}账户</td></tr><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-wrap"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;"valign="top"><table width="100%"cellpadding="0"cellspacing="0"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-block"style="font-family: 'Helvetica
 | 
			
		||||
Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;"valign="top">亲爱的<strong style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">{userName}</strong>:</td></tr><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-block"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;"valign="top">感谢您注册{siteTitle},请点击下方按钮完成账户激活。</td></tr><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-block"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;"valign="top"><a href="{activationUrl}"class="btn-primary"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #009688; margin: 0; border-color: #009688; border-style: solid; border-width: 10px 20px;">激活账户</a></td></tr><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-block"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;"valign="top">感谢您选择{siteTitle}。</td></tr></table></td></tr></table><div class="footer"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;"><table width="100%"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="aligncenter content-block"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;"align="center"valign="top">此邮件由系统自动发送,请不要直接回复。</td></tr></table></div></div></td><td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;"valign="top"></td></tr></table></body></html>`, Type: "mail_template"},
 | 
			
		||||
	{Name: "forget_captcha", Value: `0`, Type: "login"},
 | 
			
		||||
	{Name: "mail_reset_pwd_template", Value: `<!DOCTYPE html PUBLIC"-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box;
 | 
			
		||||
font-size: 14px; margin: 0;"><head><meta name="viewport"content="width=device-width"/><meta http-equiv="Content-Type"content="text/html; charset=UTF-8"/><title>重设密码</title><style type="text/css">img{max-width:100%}body{-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:none;width:100%!important;height:100%;line-height:1.6em}body{background-color:#f6f6f6}@media only screen and(max-width:640px){body{padding:0!important}h1{font-weight:800!important;margin:20px 0 5px!important}h2{font-weight:800!important;margin:20px 0 5px!important}h3{font-weight:800!important;margin:20px 0 5px!important}h4{font-weight:800!important;margin:20px 0 5px!important}h1{font-size:22px!important}h2{font-size:18px!important}h3{font-size:16px!important}.container{padding:0!important;width:100%!important}.content{padding:0!important}.content-wrap{padding:10px!important}.invoice{width:100%!important}}</style></head><body itemscope itemtype="http://schema.org/EmailMessage"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing:
 | 
			
		||||
border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;"bgcolor="#f6f6f6"><table class="body-wrap"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;"bgcolor="#f6f6f6"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif;
 | 
			
		||||
box-sizing: border-box; font-size: 14px; margin: 0;"><td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;"valign="top"></td><td class="container"width="600"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;"valign="top"><div class="content"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;"><table class="main"width="100%"cellpadding="0"cellspacing="0"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px
 | 
			
		||||
solid #e9e9e9;"bgcolor="#fff"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size:
 | 
			
		||||
14px; margin: 0;"><td class="alert alert-warning"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 16px; vertical-align: top; color: #fff; font-weight: 500; text-align: center; border-radius: 3px 3px 0 0; background-color: #2196F3; margin: 0; padding: 20px;"align="center"bgcolor="#FF9F00"valign="top">重设{siteTitle}密码</td></tr><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-wrap"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;"valign="top"><table width="100%"cellpadding="0"cellspacing="0"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-block"style="font-family: 'Helvetica
 | 
			
		||||
Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;"valign="top">亲爱的<strong style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">{userName}</strong>:</td></tr><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-block"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;"valign="top">请点击下方按钮完成密码重设。如果非你本人操作,请忽略此邮件。</td></tr><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-block"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;"valign="top"><a href="{resetUrl}"class="btn-primary"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #2196F3; margin: 0; border-color: #2196F3; border-style: solid; border-width: 10px 20px;">重设密码</a></td></tr><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-block"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;"valign="top">感谢您选择{siteTitle}。</td></tr></table></td></tr></table><div class="footer"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;"><table width="100%"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="aligncenter content-block"style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;"align="center"valign="top">此邮件由系统自动发送,请不要直接回复。</td></tr></table></div></div></td><td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;"valign="top"></td></tr></table></body></html>`, Type: "mail_template"},
 | 
			
		||||
	{Name: "pack_data", Value: `[]`, Type: "pack"},
 | 
			
		||||
	{Name: "db_version_" + conf.RequiredDBVersion, Value: `installed`, Type: "version"},
 | 
			
		||||
	{Name: "alipay_enabled", Value: `0`, Type: "payment"},
 | 
			
		||||
	{Name: "payjs_enabled", Value: `0`, Type: "payment"},
 | 
			
		||||
	{Name: "payjs_id", Value: ``, Type: "payment"},
 | 
			
		||||
	{Name: "payjs_secret", Value: ``, Type: "payment"},
 | 
			
		||||
	{Name: "appid", Value: ``, Type: "payment"},
 | 
			
		||||
	{Name: "appkey", Value: ``, Type: "payment"},
 | 
			
		||||
	{Name: "shopid", Value: ``, Type: "payment"},
 | 
			
		||||
	{Name: "wechat_enabled", Value: `0`, Type: "payment"},
 | 
			
		||||
	{Name: "wechat_appid", Value: ``, Type: "payment"},
 | 
			
		||||
	{Name: "wechat_mchid", Value: ``, Type: "payment"},
 | 
			
		||||
	{Name: "wechat_serial_no", Value: ``, Type: "payment"},
 | 
			
		||||
	{Name: "wechat_api_key", Value: ``, Type: "payment"},
 | 
			
		||||
	{Name: "wechat_pk_content", Value: ``, Type: "payment"},
 | 
			
		||||
	{Name: "hot_share_num", Value: `10`, Type: "share"},
 | 
			
		||||
	{Name: "group_sell_data", Value: `[]`, Type: "group_sell"},
 | 
			
		||||
	{Name: "gravatar_server", Value: `https://www.gravatar.com/`, Type: "avatar"},
 | 
			
		||||
	{Name: "defaultTheme", Value: `#3f51b5`, Type: "basic"},
 | 
			
		||||
	{Name: "themes", Value: `{"#3f51b5":{"palette":{"primary":{"main":"#3f51b5"},"secondary":{"main":"#f50057"}}},"#2196f3":{"palette":{"primary":{"main":"#2196f3"},"secondary":{"main":"#FFC107"}}},"#673AB7":{"palette":{"primary":{"main":"#673AB7"},"secondary":{"main":"#2196F3"}}},"#E91E63":{"palette":{"primary":{"main":"#E91E63"},"secondary":{"main":"#42A5F5","contrastText":"#fff"}}},"#FF5722":{"palette":{"primary":{"main":"#FF5722"},"secondary":{"main":"#3F51B5"}}},"#FFC107":{"palette":{"primary":{"main":"#FFC107"},"secondary":{"main":"#26C6DA"}}},"#8BC34A":{"palette":{"primary":{"main":"#8BC34A","contrastText":"#fff"},"secondary":{"main":"#FF8A65","contrastText":"#fff"}}},"#009688":{"palette":{"primary":{"main":"#009688"},"secondary":{"main":"#4DD0E1","contrastText":"#fff"}}},"#607D8B":{"palette":{"primary":{"main":"#607D8B"},"secondary":{"main":"#F06292"}}},"#795548":{"palette":{"primary":{"main":"#795548"},"secondary":{"main":"#4CAF50","contrastText":"#fff"}}}}`, Type: "basic"},
 | 
			
		||||
	{Name: "max_worker_num", Value: `10`, Type: "task"},
 | 
			
		||||
	{Name: "max_parallel_transfer", Value: `4`, Type: "task"},
 | 
			
		||||
	{Name: "secret_key", Value: util.RandStringRunes(256), Type: "auth"},
 | 
			
		||||
	{Name: "temp_path", Value: "temp", Type: "path"},
 | 
			
		||||
	{Name: "avatar_path", Value: "avatar", Type: "path"},
 | 
			
		||||
	{Name: "avatar_size", Value: "2097152", Type: "avatar"},
 | 
			
		||||
	{Name: "avatar_size_l", Value: "200", Type: "avatar"},
 | 
			
		||||
	{Name: "avatar_size_m", Value: "130", Type: "avatar"},
 | 
			
		||||
	{Name: "avatar_size_s", Value: "50", Type: "avatar"},
 | 
			
		||||
	{Name: "score_enabled", Value: "1", Type: "score"},
 | 
			
		||||
	{Name: "share_score_rate", Value: "80", Type: "score"},
 | 
			
		||||
	{Name: "score_price", Value: "1", Type: "score"},
 | 
			
		||||
	{Name: "report_enabled", Value: "0", Type: "report"},
 | 
			
		||||
	{Name: "home_view_method", Value: "list", Type: "view"},
 | 
			
		||||
	{Name: "share_view_method", Value: "list", Type: "view"},
 | 
			
		||||
	{Name: "cron_garbage_collect", Value: "@hourly", Type: "cron"},
 | 
			
		||||
	{Name: "cron_notify_user", Value: "@hourly", Type: "cron"},
 | 
			
		||||
	{Name: "cron_ban_user", Value: "@hourly", Type: "cron"},
 | 
			
		||||
	{Name: "cron_recycle_upload_session", Value: "@every 1h30m", Type: "cron"},
 | 
			
		||||
	{Name: "authn_enabled", Value: "0", Type: "authn"},
 | 
			
		||||
	{Name: "captcha_type", Value: "normal", Type: "captcha"},
 | 
			
		||||
	{Name: "captcha_height", Value: "60", Type: "captcha"},
 | 
			
		||||
	{Name: "captcha_width", Value: "240", Type: "captcha"},
 | 
			
		||||
	{Name: "captcha_mode", Value: "3", Type: "captcha"},
 | 
			
		||||
	{Name: "captcha_ComplexOfNoiseText", Value: "0", Type: "captcha"},
 | 
			
		||||
	{Name: "captcha_ComplexOfNoiseDot", Value: "0", Type: "captcha"},
 | 
			
		||||
	{Name: "captcha_IsShowHollowLine", Value: "0", Type: "captcha"},
 | 
			
		||||
	{Name: "captcha_IsShowNoiseDot", Value: "1", Type: "captcha"},
 | 
			
		||||
	{Name: "captcha_IsShowNoiseText", Value: "0", Type: "captcha"},
 | 
			
		||||
	{Name: "captcha_IsShowSlimeLine", Value: "1", Type: "captcha"},
 | 
			
		||||
	{Name: "captcha_IsShowSineLine", Value: "0", Type: "captcha"},
 | 
			
		||||
	{Name: "captcha_CaptchaLen", Value: "6", Type: "captcha"},
 | 
			
		||||
	{Name: "captcha_ReCaptchaKey", Value: "defaultKey", Type: "captcha"},
 | 
			
		||||
	{Name: "captcha_ReCaptchaSecret", Value: "defaultSecret", Type: "captcha"},
 | 
			
		||||
	{Name: "captcha_TCaptcha_CaptchaAppId", Value: "", Type: "captcha"},
 | 
			
		||||
	{Name: "captcha_TCaptcha_AppSecretKey", Value: "", Type: "captcha"},
 | 
			
		||||
	{Name: "captcha_TCaptcha_SecretId", Value: "", Type: "captcha"},
 | 
			
		||||
	{Name: "captcha_TCaptcha_SecretKey", Value: "", Type: "captcha"},
 | 
			
		||||
	{Name: "thumb_width", Value: "400", Type: "thumb"},
 | 
			
		||||
	{Name: "thumb_height", Value: "300", Type: "thumb"},
 | 
			
		||||
	{Name: "thumb_file_suffix", Value: "._thumb", Type: "thumb"},
 | 
			
		||||
	{Name: "thumb_max_task_count", Value: "-1", Type: "thumb"},
 | 
			
		||||
	{Name: "thumb_encode_method", Value: "jpg", Type: "thumb"},
 | 
			
		||||
	{Name: "thumb_gc_after_gen", Value: "0", Type: "thumb"},
 | 
			
		||||
	{Name: "thumb_encode_quality", Value: "85", Type: "thumb"},
 | 
			
		||||
	{Name: "thumb_builtin_enabled", Value: "1", Type: "thumb"},
 | 
			
		||||
	{Name: "thumb_vips_enabled", Value: "0", Type: "thumb"},
 | 
			
		||||
	{Name: "thumb_ffmpeg_enabled", Value: "0", Type: "thumb"},
 | 
			
		||||
	{Name: "thumb_vips_path", Value: "vips", Type: "thumb"},
 | 
			
		||||
	{Name: "thumb_vips_exts", Value: "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", Type: "thumb"},
 | 
			
		||||
	{Name: "thumb_ffmpeg_seek", Value: "00:00:01.00", Type: "thumb"},
 | 
			
		||||
	{Name: "thumb_ffmpeg_path", Value: "ffmpeg", Type: "thumb"},
 | 
			
		||||
	{Name: "thumb_ffmpeg_exts", Value: "3g2,3gp,asf,asx,avi,divx,flv,m2ts,m2v,m4v,mkv,mov,mp4,mpeg,mpg,mts,mxf,ogv,rm,swf,webm,wmv", Type: "thumb"},
 | 
			
		||||
	{Name: "thumb_libreoffice_path", Value: "soffice", Type: "thumb"},
 | 
			
		||||
	{Name: "thumb_libreoffice_enabled", Value: "0", Type: "thumb"},
 | 
			
		||||
	{Name: "thumb_libreoffice_exts", Value: "md,ods,ots,fods,uos,xlsx,xml,xls,xlt,dif,dbf,html,slk,csv,xlsm,docx,dotx,doc,dot,rtf,xlsm,xlst,xls,xlw,xlc,xlt,pptx,ppsx,potx,pomx,ppt,pps,ppm,pot,pom", Type: "thumb"},
 | 
			
		||||
	{Name: "thumb_proxy_enabled", Value: "0", Type: "thumb"},
 | 
			
		||||
	{Name: "thumb_proxy_policy", Value: "[]", Type: "thumb"},
 | 
			
		||||
	{Name: "thumb_max_src_size", Value: "31457280", Type: "thumb"},
 | 
			
		||||
	{Name: "pwa_small_icon", Value: "/static/img/favicon.ico", Type: "pwa"},
 | 
			
		||||
	{Name: "pwa_medium_icon", Value: "/static/img/logo192.png", Type: "pwa"},
 | 
			
		||||
	{Name: "pwa_large_icon", Value: "/static/img/logo512.png", Type: "pwa"},
 | 
			
		||||
	{Name: "pwa_display", Value: "standalone", Type: "pwa"},
 | 
			
		||||
	{Name: "pwa_theme_color", Value: "#000000", Type: "pwa"},
 | 
			
		||||
	{Name: "pwa_background_color", Value: "#ffffff", Type: "pwa"},
 | 
			
		||||
	{Name: "initial_files", Value: "[]", Type: "register"},
 | 
			
		||||
	{Name: "office_preview_service", Value: "https://view.officeapps.live.com/op/view.aspx?src={$src}", Type: "preview"},
 | 
			
		||||
	{Name: "phone_required", Value: "false", Type: "phone"},
 | 
			
		||||
	{Name: "phone_enabled", Value: "false", Type: "phone"},
 | 
			
		||||
	{Name: "vol_content", Value: "eyJkb21haW4iOiJjbG91ZHJldmUub3JnIiwicHVyY2hhc2VfZGF0ZSI6MTY3MDMyOTI3OX0=", Type: "vol"},
 | 
			
		||||
	{Name: "vol_signature", Value: "UzVBwjfFNTU1bSQV8OTgbMvTdRO7FwNYyMdTu4/phmyUltc6MrluUItiK0v+Uq6yX05L4ZnhTlojVLgi3zXWNq0Tjo3zW3CffZVwj7FCrmG72PBuQp4hV3+b/eMpUbYcTTT9zEt2mneSpGJBOsxDgaf9isVzP+J+YwynPJy1UMa1ckYlc/rEExcxqZxH1tiSHfkyuelIENDiwiggOZl7J2opM5jbxH9oTiAhxl6MN1dbY6DH9bydTibcylSXoQASCse6P/i6JmEWPSRDY22Ofkw3cqTzQcxuMSJjYYVkdAHdeqoDYi4ywmAr1tAJnlDyNNU/KmLQzufgAWjdGKTPNA==", Type: "vol"},
 | 
			
		||||
	{Name: "show_app_promotion", Value: "1", Type: "mobile"},
 | 
			
		||||
	{Name: "public_resource_maxage", Value: "86400", Type: "timeout"},
 | 
			
		||||
	{Name: "wopi_enabled", Value: "0", Type: "wopi"},
 | 
			
		||||
	{Name: "wopi_endpoint", Value: "", Type: "wopi"},
 | 
			
		||||
	{Name: "wopi_max_size", Value: "52428800", Type: "wopi"},
 | 
			
		||||
	{Name: "wopi_session_timeout", Value: "36000", Type: "wopi"},
 | 
			
		||||
	{Name: "custom_payment_enabled", Value: "0", Type: "payment"},
 | 
			
		||||
	{Name: "custom_payment_endpoint", Value: "", Type: "payment"},
 | 
			
		||||
	{Name: "custom_payment_secret", Value: "", Type: "payment"},
 | 
			
		||||
	{Name: "custom_payment_name", Value: "", Type: "payment"},
 | 
			
		||||
	{Name: "app_feedback_link", Value: "", Type: "mobile"},
 | 
			
		||||
	{Name: "app_forum_link", Value: "", Type: "mobile"},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func InitSlaveDefaults() {
 | 
			
		||||
	for _, setting := range defaultSettings {
 | 
			
		||||
		cache.Set("setting_"+setting.Name, setting.Value, -1)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										288
									
								
								models/dialects/dialect_sqlite.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										288
									
								
								models/dialects/dialect_sqlite.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,288 @@
 | 
			
		||||
package dialects
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/jinzhu/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var keyNameRegex = regexp.MustCompile("[^a-zA-Z0-9]+")
 | 
			
		||||
 | 
			
		||||
// DefaultForeignKeyNamer contains the default foreign key name generator method
 | 
			
		||||
type DefaultForeignKeyNamer struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type commonDialect struct {
 | 
			
		||||
	db gorm.SQLCommon
 | 
			
		||||
	DefaultForeignKeyNamer
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (commonDialect) GetName() string {
 | 
			
		||||
	return "common"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *commonDialect) SetDB(db gorm.SQLCommon) {
 | 
			
		||||
	s.db = db
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (commonDialect) BindVar(i int) string {
 | 
			
		||||
	return "$$$" // ?
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (commonDialect) Quote(key string) string {
 | 
			
		||||
	return fmt.Sprintf(`"%s"`, key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *commonDialect) fieldCanAutoIncrement(field *gorm.StructField) bool {
 | 
			
		||||
	if value, ok := field.TagSettingsGet("AUTO_INCREMENT"); ok {
 | 
			
		||||
		return strings.ToLower(value) != "false"
 | 
			
		||||
	}
 | 
			
		||||
	return field.IsPrimaryKey
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *commonDialect) DataTypeOf(field *gorm.StructField) string {
 | 
			
		||||
	var dataValue, sqlType, size, additionalType = gorm.ParseFieldStructForDialect(field, s)
 | 
			
		||||
 | 
			
		||||
	if sqlType == "" {
 | 
			
		||||
		switch dataValue.Kind() {
 | 
			
		||||
		case reflect.Bool:
 | 
			
		||||
			sqlType = "BOOLEAN"
 | 
			
		||||
		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr:
 | 
			
		||||
			if s.fieldCanAutoIncrement(field) {
 | 
			
		||||
				sqlType = "INTEGER AUTO_INCREMENT"
 | 
			
		||||
			} else {
 | 
			
		||||
				sqlType = "INTEGER"
 | 
			
		||||
			}
 | 
			
		||||
		case reflect.Int64, reflect.Uint64:
 | 
			
		||||
			if s.fieldCanAutoIncrement(field) {
 | 
			
		||||
				sqlType = "BIGINT AUTO_INCREMENT"
 | 
			
		||||
			} else {
 | 
			
		||||
				sqlType = "BIGINT"
 | 
			
		||||
			}
 | 
			
		||||
		case reflect.Float32, reflect.Float64:
 | 
			
		||||
			sqlType = "FLOAT"
 | 
			
		||||
		case reflect.String:
 | 
			
		||||
			if size > 0 && size < 65532 {
 | 
			
		||||
				sqlType = fmt.Sprintf("VARCHAR(%d)", size)
 | 
			
		||||
			} else {
 | 
			
		||||
				sqlType = "VARCHAR(65532)"
 | 
			
		||||
			}
 | 
			
		||||
		case reflect.Struct:
 | 
			
		||||
			if _, ok := dataValue.Interface().(time.Time); ok {
 | 
			
		||||
				sqlType = "TIMESTAMP"
 | 
			
		||||
			}
 | 
			
		||||
		default:
 | 
			
		||||
			if _, ok := dataValue.Interface().([]byte); ok {
 | 
			
		||||
				if size > 0 && size < 65532 {
 | 
			
		||||
					sqlType = fmt.Sprintf("BINARY(%d)", size)
 | 
			
		||||
				} else {
 | 
			
		||||
					sqlType = "BINARY(65532)"
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if sqlType == "" {
 | 
			
		||||
		panic(fmt.Sprintf("invalid sql type %s (%s) for commonDialect", dataValue.Type().Name(), dataValue.Kind().String()))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if strings.TrimSpace(additionalType) == "" {
 | 
			
		||||
		return sqlType
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("%v %v", sqlType, additionalType)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func currentDatabaseAndTable(dialect gorm.Dialect, tableName string) (string, string) {
 | 
			
		||||
	if strings.Contains(tableName, ".") {
 | 
			
		||||
		splitStrings := strings.SplitN(tableName, ".", 2)
 | 
			
		||||
		return splitStrings[0], splitStrings[1]
 | 
			
		||||
	}
 | 
			
		||||
	return dialect.CurrentDatabase(), tableName
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s commonDialect) HasIndex(tableName string, indexName string) bool {
 | 
			
		||||
	var count int
 | 
			
		||||
	currentDatabase, tableName := currentDatabaseAndTable(&s, tableName)
 | 
			
		||||
	s.db.QueryRow("SELECT count(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE table_schema = ? AND table_name = ? AND index_name = ?", currentDatabase, tableName, indexName).Scan(&count)
 | 
			
		||||
	return count > 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s commonDialect) RemoveIndex(tableName string, indexName string) error {
 | 
			
		||||
	_, err := s.db.Exec(fmt.Sprintf("DROP INDEX %v", indexName))
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s commonDialect) HasForeignKey(tableName string, foreignKeyName string) bool {
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s commonDialect) HasTable(tableName string) bool {
 | 
			
		||||
	var count int
 | 
			
		||||
	currentDatabase, tableName := currentDatabaseAndTable(&s, tableName)
 | 
			
		||||
	s.db.QueryRow("SELECT count(*) FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = ? AND table_name = ?", currentDatabase, tableName).Scan(&count)
 | 
			
		||||
	return count > 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s commonDialect) HasColumn(tableName string, columnName string) bool {
 | 
			
		||||
	var count int
 | 
			
		||||
	currentDatabase, tableName := currentDatabaseAndTable(&s, tableName)
 | 
			
		||||
	s.db.QueryRow("SELECT count(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = ? AND table_name = ? AND column_name = ?", currentDatabase, tableName, columnName).Scan(&count)
 | 
			
		||||
	return count > 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s commonDialect) ModifyColumn(tableName string, columnName string, typ string) error {
 | 
			
		||||
	_, err := s.db.Exec(fmt.Sprintf("ALTER TABLE %v ALTER COLUMN %v TYPE %v", tableName, columnName, typ))
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s commonDialect) CurrentDatabase() (name string) {
 | 
			
		||||
	s.db.QueryRow("SELECT DATABASE()").Scan(&name)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (commonDialect) LimitAndOffsetSQL(limit, offset interface{}) (sql string) {
 | 
			
		||||
	if limit != nil {
 | 
			
		||||
		if parsedLimit, err := strconv.ParseInt(fmt.Sprint(limit), 0, 0); err == nil && parsedLimit >= 0 {
 | 
			
		||||
			sql += fmt.Sprintf(" LIMIT %d", parsedLimit)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if offset != nil {
 | 
			
		||||
		if parsedOffset, err := strconv.ParseInt(fmt.Sprint(offset), 0, 0); err == nil && parsedOffset >= 0 {
 | 
			
		||||
			sql += fmt.Sprintf(" OFFSET %d", parsedOffset)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (commonDialect) SelectFromDummyTable() string {
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (commonDialect) LastInsertIDReturningSuffix(tableName, columnName string) string {
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (commonDialect) DefaultValueStr() string {
 | 
			
		||||
	return "DEFAULT VALUES"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BuildKeyName returns a valid key name (foreign key, index key) for the given table, field and reference
 | 
			
		||||
func (DefaultForeignKeyNamer) BuildKeyName(kind, tableName string, fields ...string) string {
 | 
			
		||||
	keyName := fmt.Sprintf("%s_%s_%s", kind, tableName, strings.Join(fields, "_"))
 | 
			
		||||
	keyName = keyNameRegex.ReplaceAllString(keyName, "_")
 | 
			
		||||
	return keyName
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NormalizeIndexAndColumn returns argument's index name and column name without doing anything
 | 
			
		||||
func (commonDialect) NormalizeIndexAndColumn(indexName, columnName string) (string, string) {
 | 
			
		||||
	return indexName, columnName
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsByteArrayOrSlice returns true of the reflected value is an array or slice
 | 
			
		||||
func IsByteArrayOrSlice(value reflect.Value) bool {
 | 
			
		||||
	return (value.Kind() == reflect.Array || value.Kind() == reflect.Slice) && value.Type().Elem() == reflect.TypeOf(uint8(0))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type sqlite struct {
 | 
			
		||||
	commonDialect
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	gorm.RegisterDialect("sqlite", &sqlite{})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (sqlite) GetName() string {
 | 
			
		||||
	return "sqlite"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get Data Type for Sqlite Dialect
 | 
			
		||||
func (s *sqlite) DataTypeOf(field *gorm.StructField) string {
 | 
			
		||||
	var dataValue, sqlType, size, additionalType = gorm.ParseFieldStructForDialect(field, s)
 | 
			
		||||
 | 
			
		||||
	if sqlType == "" {
 | 
			
		||||
		switch dataValue.Kind() {
 | 
			
		||||
		case reflect.Bool:
 | 
			
		||||
			sqlType = "bool"
 | 
			
		||||
		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr:
 | 
			
		||||
			if s.fieldCanAutoIncrement(field) {
 | 
			
		||||
				field.TagSettingsSet("AUTO_INCREMENT", "AUTO_INCREMENT")
 | 
			
		||||
				sqlType = "integer primary key autoincrement"
 | 
			
		||||
			} else {
 | 
			
		||||
				sqlType = "integer"
 | 
			
		||||
			}
 | 
			
		||||
		case reflect.Int64, reflect.Uint64:
 | 
			
		||||
			if s.fieldCanAutoIncrement(field) {
 | 
			
		||||
				field.TagSettingsSet("AUTO_INCREMENT", "AUTO_INCREMENT")
 | 
			
		||||
				sqlType = "integer primary key autoincrement"
 | 
			
		||||
			} else {
 | 
			
		||||
				sqlType = "bigint"
 | 
			
		||||
			}
 | 
			
		||||
		case reflect.Float32, reflect.Float64:
 | 
			
		||||
			sqlType = "real"
 | 
			
		||||
		case reflect.String:
 | 
			
		||||
			if size > 0 && size < 65532 {
 | 
			
		||||
				sqlType = fmt.Sprintf("varchar(%d)", size)
 | 
			
		||||
			} else {
 | 
			
		||||
				sqlType = "text"
 | 
			
		||||
			}
 | 
			
		||||
		case reflect.Struct:
 | 
			
		||||
			if _, ok := dataValue.Interface().(time.Time); ok {
 | 
			
		||||
				sqlType = "datetime"
 | 
			
		||||
			}
 | 
			
		||||
		default:
 | 
			
		||||
			if IsByteArrayOrSlice(dataValue) {
 | 
			
		||||
				sqlType = "blob"
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if sqlType == "" {
 | 
			
		||||
		panic(fmt.Sprintf("invalid sql type %s (%s) for sqlite", dataValue.Type().Name(), dataValue.Kind().String()))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if strings.TrimSpace(additionalType) == "" {
 | 
			
		||||
		return sqlType
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("%v %v", sqlType, additionalType)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s sqlite) HasIndex(tableName string, indexName string) bool {
 | 
			
		||||
	var count int
 | 
			
		||||
	s.db.QueryRow(fmt.Sprintf("SELECT count(*) FROM sqlite_master WHERE tbl_name = ? AND sql LIKE '%%INDEX %v ON%%'", indexName), tableName).Scan(&count)
 | 
			
		||||
	return count > 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s sqlite) HasTable(tableName string) bool {
 | 
			
		||||
	var count int
 | 
			
		||||
	s.db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?", tableName).Scan(&count)
 | 
			
		||||
	return count > 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s sqlite) HasColumn(tableName string, columnName string) bool {
 | 
			
		||||
	var count int
 | 
			
		||||
	s.db.QueryRow(fmt.Sprintf("SELECT count(*) FROM sqlite_master WHERE tbl_name = ? AND (sql LIKE '%%\"%v\" %%' OR sql LIKE '%%%v %%');", columnName, columnName), tableName).Scan(&count)
 | 
			
		||||
	return count > 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s sqlite) CurrentDatabase() (name string) {
 | 
			
		||||
	var (
 | 
			
		||||
		ifaces   = make([]interface{}, 3)
 | 
			
		||||
		pointers = make([]*string, 3)
 | 
			
		||||
		i        int
 | 
			
		||||
	)
 | 
			
		||||
	for i = 0; i < 3; i++ {
 | 
			
		||||
		ifaces[i] = &pointers[i]
 | 
			
		||||
	}
 | 
			
		||||
	if err := s.db.QueryRow("PRAGMA database_list").Scan(ifaces...); err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if pointers[1] != nil {
 | 
			
		||||
		name = *pointers[1]
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										128
									
								
								models/download.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								models/download.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,128 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/aria2/rpc"
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/util"
 | 
			
		||||
	"github.com/jinzhu/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Download 离线下载队列模型
 | 
			
		||||
type Download struct {
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	Status         int    // 任务状态
 | 
			
		||||
	Type           int    // 任务类型
 | 
			
		||||
	Source         string `gorm:"type:text"` // 文件下载地址
 | 
			
		||||
	TotalSize      uint64 // 文件大小
 | 
			
		||||
	DownloadedSize uint64 // 文件大小
 | 
			
		||||
	GID            string `gorm:"size:32,index:gid"` // 任务ID
 | 
			
		||||
	Speed          int    // 下载速度
 | 
			
		||||
	Parent         string `gorm:"type:text"`       // 存储目录
 | 
			
		||||
	Attrs          string `gorm:"size:4294967295"` // 任务状态属性
 | 
			
		||||
	Error          string `gorm:"type:text"`       // 错误描述
 | 
			
		||||
	Dst            string `gorm:"type:text"`       // 用户文件系统存储父目录路径
 | 
			
		||||
	UserID         uint   // 发起者UID
 | 
			
		||||
	TaskID         uint   // 对应的转存任务ID
 | 
			
		||||
	NodeID         uint   // 处理任务的节点ID
 | 
			
		||||
 | 
			
		||||
	// 关联模型
 | 
			
		||||
	User *User `gorm:"PRELOAD:false,association_autoupdate:false"`
 | 
			
		||||
 | 
			
		||||
	// 数据库忽略字段
 | 
			
		||||
	StatusInfo rpc.StatusInfo `gorm:"-"`
 | 
			
		||||
	Task       *Task          `gorm:"-"`
 | 
			
		||||
	NodeName   string         `gorm:"-"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AfterFind 找到下载任务后的钩子,处理Status结构
 | 
			
		||||
func (task *Download) AfterFind() (err error) {
 | 
			
		||||
	// 解析状态
 | 
			
		||||
	if task.Attrs != "" {
 | 
			
		||||
		err = json.Unmarshal([]byte(task.Attrs), &task.StatusInfo)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if task.TaskID != 0 {
 | 
			
		||||
		task.Task, _ = GetTasksByID(task.TaskID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BeforeSave Save下载任务前的钩子
 | 
			
		||||
func (task *Download) BeforeSave() (err error) {
 | 
			
		||||
	// 解析状态
 | 
			
		||||
	if task.Attrs != "" {
 | 
			
		||||
		err = json.Unmarshal([]byte(task.Attrs), &task.StatusInfo)
 | 
			
		||||
	}
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create 创建离线下载记录
 | 
			
		||||
func (task *Download) Create() (uint, error) {
 | 
			
		||||
	if err := DB.Create(task).Error; err != nil {
 | 
			
		||||
		util.Log().Warning("Failed to insert download record: %s", err)
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	return task.ID, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Save 更新
 | 
			
		||||
func (task *Download) Save() error {
 | 
			
		||||
	if err := DB.Save(task).Error; err != nil {
 | 
			
		||||
		util.Log().Warning("Failed to update download record: %s", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetDownloadsByStatus 根据状态检索下载
 | 
			
		||||
func GetDownloadsByStatus(status ...int) []Download {
 | 
			
		||||
	var tasks []Download
 | 
			
		||||
	DB.Where("status in (?)", status).Find(&tasks)
 | 
			
		||||
	return tasks
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetDownloadsByStatusAndUser 根据状态检索和用户ID下载
 | 
			
		||||
// page 为 0 表示列出所有,非零时分页
 | 
			
		||||
func GetDownloadsByStatusAndUser(page, uid uint, status ...int) []Download {
 | 
			
		||||
	var tasks []Download
 | 
			
		||||
	dbChain := DB
 | 
			
		||||
	if page > 0 {
 | 
			
		||||
		dbChain = dbChain.Limit(10).Offset((page - 1) * 10).Order("updated_at DESC")
 | 
			
		||||
	}
 | 
			
		||||
	dbChain.Where("user_id = ? and status in (?)", uid, status).Find(&tasks)
 | 
			
		||||
	return tasks
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetDownloadByGid 根据GID和用户ID查找下载
 | 
			
		||||
func GetDownloadByGid(gid string, uid uint) (*Download, error) {
 | 
			
		||||
	download := &Download{}
 | 
			
		||||
	result := DB.Where("user_id = ? and g_id = ?", uid, gid).First(download)
 | 
			
		||||
	return download, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetOwner 获取下载任务所属用户
 | 
			
		||||
func (task *Download) GetOwner() *User {
 | 
			
		||||
	if task.User == nil {
 | 
			
		||||
		if user, err := GetUserByID(task.UserID); err == nil {
 | 
			
		||||
			return &user
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return task.User
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Delete 删除离线下载记录
 | 
			
		||||
func (download *Download) Delete() error {
 | 
			
		||||
	return DB.Model(download).Delete(download).Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetNodeID 返回任务所属节点ID
 | 
			
		||||
func (task *Download) GetNodeID() uint {
 | 
			
		||||
	// 兼容3.4版本之前生成的下载记录
 | 
			
		||||
	if task.NodeID == 0 {
 | 
			
		||||
		return 1
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return task.NodeID
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										525
									
								
								models/file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										525
									
								
								models/file.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,525 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/gob"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/util"
 | 
			
		||||
	"github.com/jinzhu/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// File 文件
 | 
			
		||||
type File struct {
 | 
			
		||||
	// 表字段
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	Name            string `gorm:"unique_index:idx_only_one"`
 | 
			
		||||
	SourceName      string `gorm:"type:text"`
 | 
			
		||||
	UserID          uint   `gorm:"index:user_id;unique_index:idx_only_one"`
 | 
			
		||||
	Size            uint64
 | 
			
		||||
	PicInfo         string
 | 
			
		||||
	FolderID        uint `gorm:"index:folder_id;unique_index:idx_only_one"`
 | 
			
		||||
	PolicyID        uint
 | 
			
		||||
	UploadSessionID *string `gorm:"index:session_id;unique_index:session_only_one"`
 | 
			
		||||
	Metadata        string  `gorm:"type:text"`
 | 
			
		||||
 | 
			
		||||
	// 关联模型
 | 
			
		||||
	Policy Policy `gorm:"PRELOAD:false,association_autoupdate:false"`
 | 
			
		||||
 | 
			
		||||
	// 数据库忽略字段
 | 
			
		||||
	Position           string            `gorm:"-"`
 | 
			
		||||
	MetadataSerialized map[string]string `gorm:"-"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Thumb related metadata
 | 
			
		||||
const (
 | 
			
		||||
	ThumbStatusNotExist     = ""
 | 
			
		||||
	ThumbStatusExist        = "exist"
 | 
			
		||||
	ThumbStatusNotAvailable = "not_available"
 | 
			
		||||
 | 
			
		||||
	ThumbStatusMetadataKey  = "thumb_status"
 | 
			
		||||
	ThumbSidecarMetadataKey = "thumb_sidecar"
 | 
			
		||||
 | 
			
		||||
	ChecksumMetadataKey = "webdav_checksum"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	// 注册缓存用到的复杂结构
 | 
			
		||||
	gob.Register(File{})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create 创建文件记录
 | 
			
		||||
func (file *File) Create() error {
 | 
			
		||||
	tx := DB.Begin()
 | 
			
		||||
 | 
			
		||||
	if err := tx.Create(file).Error; err != nil {
 | 
			
		||||
		util.Log().Warning("Failed to insert file record: %s", err)
 | 
			
		||||
		tx.Rollback()
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user := &User{}
 | 
			
		||||
	user.ID = file.UserID
 | 
			
		||||
	if err := user.ChangeStorage(tx, "+", file.Size); err != nil {
 | 
			
		||||
		tx.Rollback()
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return tx.Commit().Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AfterFind 找到文件后的钩子
 | 
			
		||||
func (file *File) AfterFind() (err error) {
 | 
			
		||||
	// 反序列化文件元数据
 | 
			
		||||
	if file.Metadata != "" {
 | 
			
		||||
		err = json.Unmarshal([]byte(file.Metadata), &file.MetadataSerialized)
 | 
			
		||||
	} else {
 | 
			
		||||
		file.MetadataSerialized = make(map[string]string)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BeforeSave Save策略前的钩子
 | 
			
		||||
func (file *File) BeforeSave() (err error) {
 | 
			
		||||
	if len(file.MetadataSerialized) > 0 {
 | 
			
		||||
		metaValue, err := json.Marshal(&file.MetadataSerialized)
 | 
			
		||||
		file.Metadata = string(metaValue)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetChildFile 查找目录下名为name的子文件
 | 
			
		||||
func (folder *Folder) GetChildFile(name string) (*File, error) {
 | 
			
		||||
	var file File
 | 
			
		||||
	result := DB.Where("folder_id = ? AND name = ?", folder.ID, name).Find(&file)
 | 
			
		||||
 | 
			
		||||
	if result.Error == nil {
 | 
			
		||||
		file.Position = path.Join(folder.Position, folder.Name)
 | 
			
		||||
	}
 | 
			
		||||
	return &file, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetChildFiles 查找目录下子文件
 | 
			
		||||
func (folder *Folder) GetChildFiles() ([]File, error) {
 | 
			
		||||
	var files []File
 | 
			
		||||
	result := DB.Where("folder_id = ?", folder.ID).Find(&files)
 | 
			
		||||
 | 
			
		||||
	if result.Error == nil {
 | 
			
		||||
		for i := 0; i < len(files); i++ {
 | 
			
		||||
			files[i].Position = path.Join(folder.Position, folder.Name)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return files, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetFilesByIDs 根据文件ID批量获取文件,
 | 
			
		||||
// UID为0表示忽略用户,只根据文件ID检索
 | 
			
		||||
func GetFilesByIDs(ids []uint, uid uint) ([]File, error) {
 | 
			
		||||
	return GetFilesByIDsFromTX(DB, ids, uid)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetFilesByIDsFromTX(tx *gorm.DB, ids []uint, uid uint) ([]File, error) {
 | 
			
		||||
	var files []File
 | 
			
		||||
	var result *gorm.DB
 | 
			
		||||
	if uid == 0 {
 | 
			
		||||
		result = tx.Where("id in (?)", ids).Find(&files)
 | 
			
		||||
	} else {
 | 
			
		||||
		result = tx.Where("id in (?) AND user_id = ?", ids, uid).Find(&files)
 | 
			
		||||
	}
 | 
			
		||||
	return files, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetFilesByKeywords 根据关键字搜索文件,
 | 
			
		||||
// UID为0表示忽略用户,只根据文件ID检索. 如果 parents 非空, 则只限制在 parent 包含的目录下搜索
 | 
			
		||||
func GetFilesByKeywords(uid uint, parents []uint, keywords ...interface{}) ([]File, error) {
 | 
			
		||||
	var (
 | 
			
		||||
		files      []File
 | 
			
		||||
		result     = DB
 | 
			
		||||
		conditions string
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// 生成查询条件
 | 
			
		||||
	for i := 0; i < len(keywords); i++ {
 | 
			
		||||
		conditions += "name like ?"
 | 
			
		||||
		if i != len(keywords)-1 {
 | 
			
		||||
			conditions += " or "
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if uid != 0 {
 | 
			
		||||
		result = result.Where("user_id = ?", uid)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(parents) > 0 {
 | 
			
		||||
		result = result.Where("folder_id in (?)", parents)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	result = result.Where("("+conditions+")", keywords...).Find(&files)
 | 
			
		||||
 | 
			
		||||
	return files, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetChildFilesOfFolders 批量检索目录子文件
 | 
			
		||||
func GetChildFilesOfFolders(folders *[]Folder) ([]File, error) {
 | 
			
		||||
	// 将所有待检索目录ID抽离,以便检索文件
 | 
			
		||||
	folderIDs := make([]uint, 0, len(*folders))
 | 
			
		||||
	for _, value := range *folders {
 | 
			
		||||
		folderIDs = append(folderIDs, value.ID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检索文件
 | 
			
		||||
	var files []File
 | 
			
		||||
	result := DB.Where("folder_id in (?)", folderIDs).Find(&files)
 | 
			
		||||
	return files, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetUploadPlaceholderFiles 获取所有上传占位文件
 | 
			
		||||
// UID为0表示忽略用户
 | 
			
		||||
func GetUploadPlaceholderFiles(uid uint) []*File {
 | 
			
		||||
	query := DB
 | 
			
		||||
	if uid != 0 {
 | 
			
		||||
		query = query.Where("user_id = ?", uid)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var files []*File
 | 
			
		||||
	query.Where("upload_session_id is not NULL").Find(&files)
 | 
			
		||||
	return files
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetPolicy 获取文件所属策略
 | 
			
		||||
func (file *File) GetPolicy() *Policy {
 | 
			
		||||
	if file.Policy.Model.ID == 0 {
 | 
			
		||||
		file.Policy, _ = GetPolicyByID(file.PolicyID)
 | 
			
		||||
	}
 | 
			
		||||
	return &file.Policy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RemoveFilesWithSoftLinks 去除给定的文件列表中有软链接的文件
 | 
			
		||||
func RemoveFilesWithSoftLinks(files []File) ([]File, error) {
 | 
			
		||||
	// 结果值
 | 
			
		||||
	filteredFiles := make([]File, 0)
 | 
			
		||||
 | 
			
		||||
	if len(files) == 0 {
 | 
			
		||||
		return filteredFiles, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 查询软链接的文件
 | 
			
		||||
	filesWithSoftLinks := make([]File, 0)
 | 
			
		||||
	for _, file := range files {
 | 
			
		||||
		var softLinkFile File
 | 
			
		||||
		res := DB.
 | 
			
		||||
			Where("source_name = ? and policy_id = ? and id != ?", file.SourceName, file.PolicyID, file.ID).
 | 
			
		||||
			First(&softLinkFile)
 | 
			
		||||
		if res.Error == nil {
 | 
			
		||||
			filesWithSoftLinks = append(filesWithSoftLinks, softLinkFile)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 过滤具有软连接的文件
 | 
			
		||||
	// TODO: 优化复杂度
 | 
			
		||||
	if len(filesWithSoftLinks) == 0 {
 | 
			
		||||
		filteredFiles = files
 | 
			
		||||
	} else {
 | 
			
		||||
		for i := 0; i < len(files); i++ {
 | 
			
		||||
			finder := false
 | 
			
		||||
			for _, value := range filesWithSoftLinks {
 | 
			
		||||
				if value.PolicyID == files[i].PolicyID && value.SourceName == files[i].SourceName {
 | 
			
		||||
					finder = true
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if !finder {
 | 
			
		||||
				filteredFiles = append(filteredFiles, files[i])
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return filteredFiles, nil
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteFiles 批量删除文件记录并归还容量
 | 
			
		||||
func DeleteFiles(files []*File, uid uint) error {
 | 
			
		||||
	tx := DB.Begin()
 | 
			
		||||
	user := &User{}
 | 
			
		||||
	user.ID = uid
 | 
			
		||||
	var size uint64
 | 
			
		||||
	for _, file := range files {
 | 
			
		||||
		if uid > 0 && file.UserID != uid {
 | 
			
		||||
			tx.Rollback()
 | 
			
		||||
			return errors.New("user id not consistent")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		result := tx.Unscoped().Where("size = ?", file.Size).Delete(file)
 | 
			
		||||
		if result.Error != nil {
 | 
			
		||||
			tx.Rollback()
 | 
			
		||||
			return result.Error
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if result.RowsAffected == 0 {
 | 
			
		||||
			tx.Rollback()
 | 
			
		||||
			return errors.New("file size is dirty")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		size += file.Size
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if uid > 0 {
 | 
			
		||||
		if err := user.ChangeStorage(tx, "-", size); err != nil {
 | 
			
		||||
			tx.Rollback()
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return tx.Commit().Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetFilesByParentIDs 根据父目录ID查找文件
 | 
			
		||||
func GetFilesByParentIDs(ids []uint, uid uint) ([]File, error) {
 | 
			
		||||
	files := make([]File, 0, len(ids))
 | 
			
		||||
	result := DB.Where("user_id = ? and folder_id in (?)", uid, ids).Find(&files)
 | 
			
		||||
	return files, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetFilesByUploadSession 查找上传会话对应的文件
 | 
			
		||||
func GetFilesByUploadSession(sessionID string, uid uint) (*File, error) {
 | 
			
		||||
	file := File{}
 | 
			
		||||
	result := DB.Where("user_id = ? and upload_session_id = ?", uid, sessionID).Find(&file)
 | 
			
		||||
	return &file, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Rename 重命名文件
 | 
			
		||||
func (file *File) Rename(new string) error {
 | 
			
		||||
	if file.MetadataSerialized[ThumbStatusMetadataKey] == ThumbStatusNotAvailable {
 | 
			
		||||
		if !strings.EqualFold(filepath.Ext(new), filepath.Ext(file.Name)) {
 | 
			
		||||
			// Reset thumb status for new ext name.
 | 
			
		||||
			if err := file.resetThumb(); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return DB.Model(&file).Set("gorm:association_autoupdate", false).Updates(map[string]interface{}{
 | 
			
		||||
		"name":     new,
 | 
			
		||||
		"metadata": file.Metadata,
 | 
			
		||||
	}).Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdatePicInfo 更新文件的图像信息
 | 
			
		||||
func (file *File) UpdatePicInfo(value string) error {
 | 
			
		||||
	return DB.Model(&file).Set("gorm:association_autoupdate", false).UpdateColumns(File{PicInfo: value}).Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateMetadata 新增或修改文件的元信息
 | 
			
		||||
func (file *File) UpdateMetadata(data map[string]string) error {
 | 
			
		||||
	if file.MetadataSerialized == nil {
 | 
			
		||||
		file.MetadataSerialized = make(map[string]string)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for k, v := range data {
 | 
			
		||||
		file.MetadataSerialized[k] = v
 | 
			
		||||
	}
 | 
			
		||||
	metaValue, err := json.Marshal(&file.MetadataSerialized)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return DB.Model(&file).Set("gorm:association_autoupdate", false).UpdateColumns(File{Metadata: string(metaValue)}).Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateSize 更新文件的大小信息
 | 
			
		||||
// TODO: 全局锁
 | 
			
		||||
func (file *File) UpdateSize(value uint64) error {
 | 
			
		||||
	tx := DB.Begin()
 | 
			
		||||
	var sizeDelta uint64
 | 
			
		||||
	operator := "+"
 | 
			
		||||
	user := User{}
 | 
			
		||||
	user.ID = file.UserID
 | 
			
		||||
	if value > file.Size {
 | 
			
		||||
		sizeDelta = value - file.Size
 | 
			
		||||
	} else {
 | 
			
		||||
		operator = "-"
 | 
			
		||||
		sizeDelta = file.Size - value
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := file.resetThumb(); err != nil {
 | 
			
		||||
		tx.Rollback()
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res := tx.Model(&file).
 | 
			
		||||
		Where("size = ?", file.Size).
 | 
			
		||||
		Set("gorm:association_autoupdate", false).
 | 
			
		||||
		Updates(map[string]interface{}{
 | 
			
		||||
			"size":     value,
 | 
			
		||||
			"metadata": file.Metadata,
 | 
			
		||||
		}); res.Error != nil {
 | 
			
		||||
		tx.Rollback()
 | 
			
		||||
		return res.Error
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := user.ChangeStorage(tx, operator, sizeDelta); err != nil {
 | 
			
		||||
		tx.Rollback()
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	file.Size = value
 | 
			
		||||
	return tx.Commit().Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateSourceName 更新文件的源文件名
 | 
			
		||||
func (file *File) UpdateSourceName(value string) error {
 | 
			
		||||
	if err := file.resetThumb(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return DB.Model(&file).Set("gorm:association_autoupdate", false).Updates(map[string]interface{}{
 | 
			
		||||
		"source_name": value,
 | 
			
		||||
		"metadata":    file.Metadata,
 | 
			
		||||
	}).Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Relocate 更新文件的物理指向
 | 
			
		||||
func (file *File) Relocate(src string, policyID uint) error {
 | 
			
		||||
	file.Policy = Policy{}
 | 
			
		||||
	return DB.Model(&file).Set("gorm:association_autoupdate", false).Updates(map[string]interface{}{
 | 
			
		||||
		"source_name": src,
 | 
			
		||||
		"policy_id":   policyID,
 | 
			
		||||
	}).Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (file *File) PopChunkToFile(lastModified *time.Time, picInfo string) error {
 | 
			
		||||
	file.UploadSessionID = nil
 | 
			
		||||
	if lastModified != nil {
 | 
			
		||||
		file.UpdatedAt = *lastModified
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return DB.Model(file).UpdateColumns(map[string]interface{}{
 | 
			
		||||
		"upload_session_id": file.UploadSessionID,
 | 
			
		||||
		"updated_at":        file.UpdatedAt,
 | 
			
		||||
		"pic_info":          picInfo,
 | 
			
		||||
	}).Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CanCopy 返回文件是否可被复制
 | 
			
		||||
func (file *File) CanCopy() bool {
 | 
			
		||||
	return file.UploadSessionID == nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateOrGetSourceLink creates a SourceLink model. If the given model exists, the existing
 | 
			
		||||
// model will be returned.
 | 
			
		||||
func (file *File) CreateOrGetSourceLink() (*SourceLink, error) {
 | 
			
		||||
	res := &SourceLink{}
 | 
			
		||||
	err := DB.Set("gorm:auto_preload", true).Where("file_id = ?", file.ID).Find(&res).Error
 | 
			
		||||
	if err == nil && res.ID > 0 {
 | 
			
		||||
		return res, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res.FileID = file.ID
 | 
			
		||||
	res.Name = file.Name
 | 
			
		||||
	if err := DB.Save(res).Error; err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to insert SourceLink: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res.File = *file
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (file *File) resetThumb() error {
 | 
			
		||||
	if _, ok := file.MetadataSerialized[ThumbStatusMetadataKey]; !ok {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	delete(file.MetadataSerialized, ThumbStatusMetadataKey)
 | 
			
		||||
	metaValue, err := json.Marshal(&file.MetadataSerialized)
 | 
			
		||||
	file.Metadata = string(metaValue)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
	实现 webdav.FileInfo 接口
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
func (file *File) GetName() string {
 | 
			
		||||
	return file.Name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (file *File) GetSize() uint64 {
 | 
			
		||||
	return file.Size
 | 
			
		||||
}
 | 
			
		||||
func (file *File) ModTime() time.Time {
 | 
			
		||||
	return file.UpdatedAt
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (file *File) IsDir() bool {
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (file *File) GetPosition() string {
 | 
			
		||||
	return file.Position
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ShouldLoadThumb returns if file explorer should try to load thumbnail for this file.
 | 
			
		||||
// `True` does not guarantee the load request will success in next step, but the client
 | 
			
		||||
// should try to load and fallback to default placeholder in case error returned.
 | 
			
		||||
func (file *File) ShouldLoadThumb() bool {
 | 
			
		||||
	return file.MetadataSerialized[ThumbStatusMetadataKey] != ThumbStatusNotAvailable
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// return sidecar thumb file name
 | 
			
		||||
func (file *File) ThumbFile() string {
 | 
			
		||||
	return file.SourceName + GetSettingByNameWithDefault("thumb_file_suffix", "._thumb")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
	实现 filesystem.FileHeader 接口
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
// Read 实现 io.Reader
 | 
			
		||||
func (file *File) Read(p []byte) (n int, err error) {
 | 
			
		||||
	return 0, errors.New("noe supported")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Close 实现io.Closer
 | 
			
		||||
func (file *File) Close() error {
 | 
			
		||||
	return errors.New("noe supported")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Seeker 实现io.Seeker
 | 
			
		||||
func (file *File) Seek(offset int64, whence int) (int64, error) {
 | 
			
		||||
	return 0, errors.New("noe supported")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (file *File) Info() *fsctx.UploadTaskInfo {
 | 
			
		||||
	return &fsctx.UploadTaskInfo{
 | 
			
		||||
		Size:            file.Size,
 | 
			
		||||
		FileName:        file.Name,
 | 
			
		||||
		VirtualPath:     file.Position,
 | 
			
		||||
		Mode:            0,
 | 
			
		||||
		Metadata:        file.MetadataSerialized,
 | 
			
		||||
		LastModified:    &file.UpdatedAt,
 | 
			
		||||
		SavePath:        file.SourceName,
 | 
			
		||||
		UploadSessionID: file.UploadSessionID,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (file *File) SetSize(size uint64) {
 | 
			
		||||
	file.Size = size
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (file *File) SetModel(newFile interface{}) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (file *File) Seekable() bool {
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										365
									
								
								models/folder.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										365
									
								
								models/folder.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,365 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"path"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/util"
 | 
			
		||||
	"github.com/jinzhu/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Folder 目录
 | 
			
		||||
type Folder struct {
 | 
			
		||||
	// 表字段
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	Name     string `gorm:"unique_index:idx_only_one_name"`
 | 
			
		||||
	ParentID *uint  `gorm:"index:parent_id;unique_index:idx_only_one_name"`
 | 
			
		||||
	OwnerID  uint   `gorm:"index:owner_id"`
 | 
			
		||||
	PolicyID uint   // Webdav下挂载的存储策略ID
 | 
			
		||||
 | 
			
		||||
	// 数据库忽略字段
 | 
			
		||||
	Position        string `gorm:"-"`
 | 
			
		||||
	WebdavDstName   string `gorm:"-"`
 | 
			
		||||
	InheritPolicyID uint   `gorm:"-"` //  从父目录继承而来的policy id,默认值则使用自身的的PolicyID
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create 创建目录
 | 
			
		||||
func (folder *Folder) Create() (uint, error) {
 | 
			
		||||
	if err := DB.FirstOrCreate(folder, *folder).Error; err != nil {
 | 
			
		||||
		folder.Model = gorm.Model{}
 | 
			
		||||
		err2 := DB.First(folder, *folder).Error
 | 
			
		||||
		return folder.ID, err2
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return folder.ID, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetMountedFolders 列出已挂载存储策略的目录
 | 
			
		||||
func GetMountedFolders(uid uint) []Folder {
 | 
			
		||||
	var folders []Folder
 | 
			
		||||
	DB.Where("owner_id = ? and policy_id <> ?", uid, 0).Find(&folders)
 | 
			
		||||
	return folders
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetChild 返回folder下名为name的子目录,不存在则返回错误
 | 
			
		||||
func (folder *Folder) GetChild(name string) (*Folder, error) {
 | 
			
		||||
	var resFolder Folder
 | 
			
		||||
	err := DB.
 | 
			
		||||
		Where("parent_id = ? AND owner_id = ? AND name = ?", folder.ID, folder.OwnerID, name).
 | 
			
		||||
		First(&resFolder).Error
 | 
			
		||||
 | 
			
		||||
	// 将子目录的路径及存储策略传递下去
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		resFolder.Position = path.Join(folder.Position, folder.Name)
 | 
			
		||||
		if folder.PolicyID > 0 {
 | 
			
		||||
			resFolder.InheritPolicyID = folder.PolicyID
 | 
			
		||||
		} else if folder.InheritPolicyID > 0 {
 | 
			
		||||
			resFolder.InheritPolicyID = folder.InheritPolicyID
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return &resFolder, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TraceRoot 向上递归查找父目录
 | 
			
		||||
func (folder *Folder) TraceRoot() error {
 | 
			
		||||
	if folder.ParentID == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var parentFolder Folder
 | 
			
		||||
	err := DB.
 | 
			
		||||
		Where("id = ? AND owner_id = ?", folder.ParentID, folder.OwnerID).
 | 
			
		||||
		First(&parentFolder).Error
 | 
			
		||||
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		err := parentFolder.TraceRoot()
 | 
			
		||||
		folder.Position = path.Join(parentFolder.Position, parentFolder.Name)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetChildFolder 查找子目录
 | 
			
		||||
func (folder *Folder) GetChildFolder() ([]Folder, error) {
 | 
			
		||||
	var folders []Folder
 | 
			
		||||
	result := DB.Where("parent_id = ?", folder.ID).Find(&folders)
 | 
			
		||||
 | 
			
		||||
	if result.Error == nil {
 | 
			
		||||
		for i := 0; i < len(folders); i++ {
 | 
			
		||||
			folders[i].Position = path.Join(folder.Position, folder.Name)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return folders, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetRecursiveChildFolder 查找所有递归子目录,包括自身
 | 
			
		||||
func GetRecursiveChildFolder(dirs []uint, uid uint, includeSelf bool) ([]Folder, error) {
 | 
			
		||||
	folders := make([]Folder, 0, len(dirs))
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	var parFolders []Folder
 | 
			
		||||
	result := DB.Where("owner_id = ? and id in (?)", uid, dirs).Find(&parFolders)
 | 
			
		||||
	if result.Error != nil {
 | 
			
		||||
		return folders, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 整理父目录的ID
 | 
			
		||||
	var parentIDs = make([]uint, 0, len(parFolders))
 | 
			
		||||
	for _, folder := range parFolders {
 | 
			
		||||
		parentIDs = append(parentIDs, folder.ID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if includeSelf {
 | 
			
		||||
		// 合并至最终结果
 | 
			
		||||
		folders = append(folders, parFolders...)
 | 
			
		||||
	}
 | 
			
		||||
	parFolders = []Folder{}
 | 
			
		||||
 | 
			
		||||
	// 递归查询子目录,最大递归65535次
 | 
			
		||||
	for i := 0; i < 65535; i++ {
 | 
			
		||||
 | 
			
		||||
		result = DB.Where("owner_id = ? and parent_id in (?)", uid, parentIDs).Find(&parFolders)
 | 
			
		||||
 | 
			
		||||
		// 查询结束条件
 | 
			
		||||
		if len(parFolders) == 0 {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 整理父目录的ID
 | 
			
		||||
		parentIDs = make([]uint, 0, len(parFolders))
 | 
			
		||||
		for _, folder := range parFolders {
 | 
			
		||||
			parentIDs = append(parentIDs, folder.ID)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 合并至最终结果
 | 
			
		||||
		folders = append(folders, parFolders...)
 | 
			
		||||
		parFolders = []Folder{}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return folders, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteFolderByIDs 根据给定ID批量删除目录记录
 | 
			
		||||
func DeleteFolderByIDs(ids []uint) error {
 | 
			
		||||
	result := DB.Where("id in (?)", ids).Unscoped().Delete(&Folder{})
 | 
			
		||||
	return result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetFoldersByIDs 根据ID和用户查找所有目录
 | 
			
		||||
func GetFoldersByIDs(ids []uint, uid uint) ([]Folder, error) {
 | 
			
		||||
	var folders []Folder
 | 
			
		||||
	result := DB.Where("id in (?) AND owner_id = ?", ids, uid).Find(&folders)
 | 
			
		||||
	return folders, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MoveOrCopyFileTo 将此目录下的files移动或复制至dstFolder,
 | 
			
		||||
// 返回此操作新增的容量
 | 
			
		||||
func (folder *Folder) MoveOrCopyFileTo(files []uint, dstFolder *Folder, isCopy bool) (uint64, error) {
 | 
			
		||||
	// 已复制文件的总大小
 | 
			
		||||
	var copiedSize uint64
 | 
			
		||||
 | 
			
		||||
	if isCopy {
 | 
			
		||||
		// 检索出要复制的文件
 | 
			
		||||
		var originFiles = make([]File, 0, len(files))
 | 
			
		||||
		if err := DB.Where(
 | 
			
		||||
			"id in (?) and user_id = ? and folder_id = ?",
 | 
			
		||||
			files,
 | 
			
		||||
			folder.OwnerID,
 | 
			
		||||
			folder.ID,
 | 
			
		||||
		).Find(&originFiles).Error; err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 复制文件记录
 | 
			
		||||
		for _, oldFile := range originFiles {
 | 
			
		||||
			if !oldFile.CanCopy() {
 | 
			
		||||
				util.Log().Warning("Cannot copy file %q because it's being uploaded now, skipping...", oldFile.Name)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			oldFile.Model = gorm.Model{}
 | 
			
		||||
			oldFile.FolderID = dstFolder.ID
 | 
			
		||||
			oldFile.UserID = dstFolder.OwnerID
 | 
			
		||||
 | 
			
		||||
			// webdav目标名重置
 | 
			
		||||
			if dstFolder.WebdavDstName != "" {
 | 
			
		||||
				oldFile.Name = dstFolder.WebdavDstName
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err := DB.Create(&oldFile).Error; err != nil {
 | 
			
		||||
				return copiedSize, err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			copiedSize += oldFile.Size
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	} else {
 | 
			
		||||
		var updates = map[string]interface{}{
 | 
			
		||||
			"folder_id": dstFolder.ID,
 | 
			
		||||
		}
 | 
			
		||||
		// webdav目标名重置
 | 
			
		||||
		if dstFolder.WebdavDstName != "" {
 | 
			
		||||
			updates["name"] = dstFolder.WebdavDstName
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 更改顶级要移动文件的父目录指向
 | 
			
		||||
		err := DB.Model(File{}).Where(
 | 
			
		||||
			"id in (?) and user_id = ? and folder_id = ?",
 | 
			
		||||
			files,
 | 
			
		||||
			folder.OwnerID,
 | 
			
		||||
			folder.ID,
 | 
			
		||||
		).
 | 
			
		||||
			Update(updates).
 | 
			
		||||
			Error
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return copiedSize, nil
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CopyFolderTo 将此目录及其子目录及文件递归复制至dstFolder
 | 
			
		||||
// 返回此操作新增的容量
 | 
			
		||||
func (folder *Folder) CopyFolderTo(folderID uint, dstFolder *Folder) (size uint64, err error) {
 | 
			
		||||
	// 列出所有子目录
 | 
			
		||||
	subFolders, err := GetRecursiveChildFolder([]uint{folderID}, folder.OwnerID, true)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 抽离所有子目录的ID
 | 
			
		||||
	var subFolderIDs = make([]uint, len(subFolders))
 | 
			
		||||
	for key, value := range subFolders {
 | 
			
		||||
		subFolderIDs[key] = value.ID
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 复制子目录
 | 
			
		||||
	var newIDCache = make(map[uint]uint)
 | 
			
		||||
	for _, folder := range subFolders {
 | 
			
		||||
		// 新的父目录指向
 | 
			
		||||
		var newID uint
 | 
			
		||||
		// 顶级目录直接指向新的目的目录
 | 
			
		||||
		if folder.ID == folderID {
 | 
			
		||||
			newID = dstFolder.ID
 | 
			
		||||
			// webdav目标名重置
 | 
			
		||||
			if dstFolder.WebdavDstName != "" {
 | 
			
		||||
				folder.Name = dstFolder.WebdavDstName
 | 
			
		||||
			}
 | 
			
		||||
		} else if IDCache, ok := newIDCache[*folder.ParentID]; ok {
 | 
			
		||||
			newID = IDCache
 | 
			
		||||
		} else {
 | 
			
		||||
			util.Log().Warning("Failed to get parent folder %q", *folder.ParentID)
 | 
			
		||||
			return size, errors.New("Failed to get parent folder")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 插入新的目录记录
 | 
			
		||||
		oldID := folder.ID
 | 
			
		||||
		folder.Model = gorm.Model{}
 | 
			
		||||
		folder.ParentID = &newID
 | 
			
		||||
		folder.OwnerID = dstFolder.OwnerID
 | 
			
		||||
		if err = DB.Create(&folder).Error; err != nil {
 | 
			
		||||
			return size, err
 | 
			
		||||
		}
 | 
			
		||||
		// 记录新的ID以便其子目录使用
 | 
			
		||||
		newIDCache[oldID] = folder.ID
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 复制文件
 | 
			
		||||
	var originFiles = make([]File, 0, len(subFolderIDs))
 | 
			
		||||
	if err := DB.Where(
 | 
			
		||||
		"user_id = ? and folder_id in (?)",
 | 
			
		||||
		folder.OwnerID,
 | 
			
		||||
		subFolderIDs,
 | 
			
		||||
	).Find(&originFiles).Error; err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 复制文件记录
 | 
			
		||||
	for _, oldFile := range originFiles {
 | 
			
		||||
		if !oldFile.CanCopy() {
 | 
			
		||||
			util.Log().Warning("Cannot copy file %q because it's being uploaded now, skipping...", oldFile.Name)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		oldFile.Model = gorm.Model{}
 | 
			
		||||
		oldFile.FolderID = newIDCache[oldFile.FolderID]
 | 
			
		||||
		oldFile.UserID = dstFolder.OwnerID
 | 
			
		||||
		if err := DB.Create(&oldFile).Error; err != nil {
 | 
			
		||||
			return size, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		size += oldFile.Size
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return size, nil
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MoveFolderTo 将folder目录下的dirs子目录复制或移动到dstFolder,
 | 
			
		||||
// 返回此过程中增加的容量
 | 
			
		||||
func (folder *Folder) MoveFolderTo(dirs []uint, dstFolder *Folder) error {
 | 
			
		||||
 | 
			
		||||
	// 如果目标位置为待移动的目录,会导致 parent 为自己
 | 
			
		||||
	// 造成死循环且无法被除搜索以外的组件展示
 | 
			
		||||
	if folder.OwnerID == dstFolder.OwnerID && util.ContainsUint(dirs, dstFolder.ID) {
 | 
			
		||||
		return errors.New("cannot move a folder into itself")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var updates = map[string]interface{}{
 | 
			
		||||
		"parent_id": dstFolder.ID,
 | 
			
		||||
	}
 | 
			
		||||
	// webdav目标名重置
 | 
			
		||||
	if dstFolder.WebdavDstName != "" {
 | 
			
		||||
		updates["name"] = dstFolder.WebdavDstName
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 更改顶级要移动目录的父目录指向
 | 
			
		||||
	err := DB.Model(Folder{}).Where(
 | 
			
		||||
		"id in (?) and owner_id = ? and parent_id = ?",
 | 
			
		||||
		dirs,
 | 
			
		||||
		folder.OwnerID,
 | 
			
		||||
		folder.ID,
 | 
			
		||||
	).Update(updates).Error
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Rename 重命名目录
 | 
			
		||||
func (folder *Folder) Rename(new string) error {
 | 
			
		||||
	return DB.Model(&folder).UpdateColumn("name", new).Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Mount 目录挂载
 | 
			
		||||
func (folder *Folder) Mount(new uint) error {
 | 
			
		||||
	return DB.Model(&folder).Update("policy_id", new).Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
	实现 FileInfo.FileInfo 接口
 | 
			
		||||
	TODO 测试
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
func (folder *Folder) GetName() string {
 | 
			
		||||
	return folder.Name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (folder *Folder) GetSize() uint64 {
 | 
			
		||||
	return 0
 | 
			
		||||
}
 | 
			
		||||
func (folder *Folder) ModTime() time.Time {
 | 
			
		||||
	return folder.UpdatedAt
 | 
			
		||||
}
 | 
			
		||||
func (folder *Folder) IsDir() bool {
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
func (folder *Folder) GetPosition() string {
 | 
			
		||||
	return folder.Position
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										89
									
								
								models/group.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								models/group.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
 | 
			
		||||
	"github.com/jinzhu/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Group 用户组模型
 | 
			
		||||
type Group struct {
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	Name          string
 | 
			
		||||
	Policies      string
 | 
			
		||||
	MaxStorage    uint64
 | 
			
		||||
	ShareEnabled  bool
 | 
			
		||||
	WebDAVEnabled bool
 | 
			
		||||
	SpeedLimit    int
 | 
			
		||||
	Options       string `json:"-" gorm:"size:4294967295"`
 | 
			
		||||
 | 
			
		||||
	// 数据库忽略字段
 | 
			
		||||
	PolicyList        []uint      `gorm:"-"`
 | 
			
		||||
	OptionsSerialized GroupOption `gorm:"-"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GroupOption 用户组其他配置
 | 
			
		||||
type GroupOption struct {
 | 
			
		||||
	ArchiveDownload  bool                   `json:"archive_download,omitempty"` // 打包下载
 | 
			
		||||
	ArchiveTask      bool                   `json:"archive_task,omitempty"`     // 在线压缩
 | 
			
		||||
	CompressSize     uint64                 `json:"compress_size,omitempty"`    // 可压缩大小
 | 
			
		||||
	DecompressSize   uint64                 `json:"decompress_size,omitempty"`
 | 
			
		||||
	OneTimeDownload  bool                   `json:"one_time_download,omitempty"`
 | 
			
		||||
	ShareDownload    bool                   `json:"share_download,omitempty"`
 | 
			
		||||
	ShareFree        bool                   `json:"share_free,omitempty"`
 | 
			
		||||
	Aria2            bool                   `json:"aria2,omitempty"`         // 离线下载
 | 
			
		||||
	Aria2Options     map[string]interface{} `json:"aria2_options,omitempty"` // 离线下载用户组配置
 | 
			
		||||
	Relocate         bool                   `json:"relocate,omitempty"`      // 转移文件
 | 
			
		||||
	SourceBatchSize  int                    `json:"source_batch,omitempty"`
 | 
			
		||||
	RedirectedSource bool                   `json:"redirected_source,omitempty"`
 | 
			
		||||
	Aria2BatchSize   int                    `json:"aria2_batch,omitempty"`
 | 
			
		||||
	AvailableNodes   []uint                 `json:"available_nodes,omitempty"`
 | 
			
		||||
	SelectNode       bool                   `json:"select_node,omitempty"`
 | 
			
		||||
	AdvanceDelete    bool                   `json:"advance_delete,omitempty"`
 | 
			
		||||
	WebDAVProxy      bool                   `json:"webdav_proxy,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetGroupByID 用ID获取用户组
 | 
			
		||||
func GetGroupByID(ID interface{}) (Group, error) {
 | 
			
		||||
	var group Group
 | 
			
		||||
	result := DB.First(&group, ID)
 | 
			
		||||
	return group, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AfterFind 找到用户组后的钩子,处理Policy列表
 | 
			
		||||
func (group *Group) AfterFind() (err error) {
 | 
			
		||||
	// 解析用户组策略列表
 | 
			
		||||
	if group.Policies != "" {
 | 
			
		||||
		err = json.Unmarshal([]byte(group.Policies), &group.PolicyList)
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 解析用户组设置
 | 
			
		||||
	if group.Options != "" {
 | 
			
		||||
		err = json.Unmarshal([]byte(group.Options), &group.OptionsSerialized)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BeforeSave Save用户前的钩子
 | 
			
		||||
func (group *Group) BeforeSave() (err error) {
 | 
			
		||||
	err = group.SerializePolicyList()
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SerializePolicyList 将序列后的可选策略列表、配置写入数据库字段
 | 
			
		||||
// TODO 完善测试
 | 
			
		||||
func (group *Group) SerializePolicyList() (err error) {
 | 
			
		||||
	policies, err := json.Marshal(&group.PolicyList)
 | 
			
		||||
	group.Policies = string(policies)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	optionsValue, err := json.Marshal(&group.OptionsSerialized)
 | 
			
		||||
	group.Options = string(optionsValue)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										106
									
								
								models/init.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								models/init.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,106 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/conf"
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/util"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/jinzhu/gorm"
 | 
			
		||||
 | 
			
		||||
	_ "github.com/cloudreve/Cloudreve/v3/models/dialects"
 | 
			
		||||
	_ "github.com/glebarez/go-sqlite"
 | 
			
		||||
	_ "github.com/jinzhu/gorm/dialects/mssql"
 | 
			
		||||
	_ "github.com/jinzhu/gorm/dialects/mysql"
 | 
			
		||||
	_ "github.com/jinzhu/gorm/dialects/postgres"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// DB 数据库链接单例
 | 
			
		||||
var DB *gorm.DB
 | 
			
		||||
 | 
			
		||||
// Init 初始化 MySQL 链接
 | 
			
		||||
func Init() {
 | 
			
		||||
	util.Log().Info("Initializing database connection...")
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		db         *gorm.DB
 | 
			
		||||
		err        error
 | 
			
		||||
		confDBType string = conf.DatabaseConfig.Type
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// 兼容已有配置中的 "sqlite3" 配置项
 | 
			
		||||
	if confDBType == "sqlite3" {
 | 
			
		||||
		confDBType = "sqlite"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if gin.Mode() == gin.TestMode {
 | 
			
		||||
		// 测试模式下,使用内存数据库
 | 
			
		||||
		db, err = gorm.Open("sqlite", ":memory:")
 | 
			
		||||
	} else {
 | 
			
		||||
		switch confDBType {
 | 
			
		||||
		case "UNSET", "sqlite":
 | 
			
		||||
			// 未指定数据库或者明确指定为 sqlite 时,使用 SQLite 数据库
 | 
			
		||||
			db, err = gorm.Open("sqlite", util.RelativePath(conf.DatabaseConfig.DBFile))
 | 
			
		||||
		case "postgres":
 | 
			
		||||
			db, err = gorm.Open(confDBType, fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable",
 | 
			
		||||
				conf.DatabaseConfig.Host,
 | 
			
		||||
				conf.DatabaseConfig.User,
 | 
			
		||||
				conf.DatabaseConfig.Password,
 | 
			
		||||
				conf.DatabaseConfig.Name,
 | 
			
		||||
				conf.DatabaseConfig.Port))
 | 
			
		||||
		case "mysql", "mssql":
 | 
			
		||||
			var host string
 | 
			
		||||
			if conf.DatabaseConfig.UnixSocket {
 | 
			
		||||
				host = fmt.Sprintf("unix(%s)",
 | 
			
		||||
					conf.DatabaseConfig.Host)
 | 
			
		||||
			} else {
 | 
			
		||||
				host = fmt.Sprintf("(%s:%d)",
 | 
			
		||||
					conf.DatabaseConfig.Host,
 | 
			
		||||
					conf.DatabaseConfig.Port)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			db, err = gorm.Open(confDBType, fmt.Sprintf("%s:%s@%s/%s?charset=%s&parseTime=True&loc=Local",
 | 
			
		||||
				conf.DatabaseConfig.User,
 | 
			
		||||
				conf.DatabaseConfig.Password,
 | 
			
		||||
				host,
 | 
			
		||||
				conf.DatabaseConfig.Name,
 | 
			
		||||
				conf.DatabaseConfig.Charset))
 | 
			
		||||
		default:
 | 
			
		||||
			util.Log().Panic("Unsupported database type %q.", confDBType)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//db.SetLogger(util.Log())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		util.Log().Panic("Failed to connect to database: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 处理表前缀
 | 
			
		||||
	gorm.DefaultTableNameHandler = func(db *gorm.DB, defaultTableName string) string {
 | 
			
		||||
		return conf.DatabaseConfig.TablePrefix + defaultTableName
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Debug模式下,输出所有 SQL 日志
 | 
			
		||||
	if conf.SystemConfig.Debug {
 | 
			
		||||
		db.LogMode(true)
 | 
			
		||||
	} else {
 | 
			
		||||
		db.LogMode(false)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//设置连接池
 | 
			
		||||
	db.DB().SetMaxIdleConns(50)
 | 
			
		||||
	if confDBType == "sqlite" || confDBType == "UNSET" {
 | 
			
		||||
		db.DB().SetMaxOpenConns(1)
 | 
			
		||||
	} else {
 | 
			
		||||
		db.DB().SetMaxOpenConns(100)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//超时
 | 
			
		||||
	db.DB().SetConnMaxLifetime(time.Second * 30)
 | 
			
		||||
 | 
			
		||||
	DB = db
 | 
			
		||||
 | 
			
		||||
	//执行迁移
 | 
			
		||||
	migration()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										221
									
								
								models/migration.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								models/migration.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,221 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/models/scripts/invoker"
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/cache"
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/conf"
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/util"
 | 
			
		||||
	"github.com/fatih/color"
 | 
			
		||||
	"github.com/hashicorp/go-version"
 | 
			
		||||
	"github.com/jinzhu/gorm"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 是否需要迁移
 | 
			
		||||
func needMigration() bool {
 | 
			
		||||
	var setting Setting
 | 
			
		||||
	return DB.Where("name = ?", "db_version_"+conf.RequiredDBVersion).First(&setting).Error != nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 执行数据迁移
 | 
			
		||||
func migration() {
 | 
			
		||||
	// 确认是否需要执行迁移
 | 
			
		||||
	if !needMigration() {
 | 
			
		||||
		util.Log().Info("Database version fulfilled, skip schema migration.")
 | 
			
		||||
		return
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	util.Log().Info("Start initializing database schema...")
 | 
			
		||||
 | 
			
		||||
	// 清除所有缓存
 | 
			
		||||
	if instance, ok := cache.Store.(*cache.RedisStore); ok {
 | 
			
		||||
		instance.DeleteAll()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 自动迁移模式
 | 
			
		||||
	if conf.DatabaseConfig.Type == "mysql" {
 | 
			
		||||
		DB = DB.Set("gorm:table_options", "ENGINE=InnoDB")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	DB.AutoMigrate(&User{}, &Setting{}, &Group{}, &Policy{}, &Folder{}, &File{}, &StoragePack{}, &Share{},
 | 
			
		||||
		&Task{}, &Download{}, &Tag{}, &Webdav{}, &Order{}, &Redeem{}, &Report{}, &Node{}, &SourceLink{})
 | 
			
		||||
 | 
			
		||||
	// 创建初始存储策略
 | 
			
		||||
	addDefaultPolicy()
 | 
			
		||||
 | 
			
		||||
	// 创建初始用户组
 | 
			
		||||
	addDefaultGroups()
 | 
			
		||||
 | 
			
		||||
	// 创建初始管理员账户
 | 
			
		||||
	addDefaultUser()
 | 
			
		||||
 | 
			
		||||
	// 创建初始节点
 | 
			
		||||
	addDefaultNode()
 | 
			
		||||
 | 
			
		||||
	// 向设置数据表添加初始设置
 | 
			
		||||
	addDefaultSettings()
 | 
			
		||||
 | 
			
		||||
	// 执行数据库升级脚本
 | 
			
		||||
	execUpgradeScripts()
 | 
			
		||||
 | 
			
		||||
	util.Log().Info("Finish initializing database schema.")
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func addDefaultPolicy() {
 | 
			
		||||
	_, err := GetPolicyByID(uint(1))
 | 
			
		||||
	// 未找到初始存储策略时,则创建
 | 
			
		||||
	if gorm.IsRecordNotFoundError(err) {
 | 
			
		||||
		defaultPolicy := Policy{
 | 
			
		||||
			Name:               "Default storage policy",
 | 
			
		||||
			Type:               "local",
 | 
			
		||||
			MaxSize:            0,
 | 
			
		||||
			AutoRename:         true,
 | 
			
		||||
			DirNameRule:        "uploads/{uid}/{path}",
 | 
			
		||||
			FileNameRule:       "{uid}_{randomkey8}_{originname}",
 | 
			
		||||
			IsOriginLinkEnable: false,
 | 
			
		||||
			OptionsSerialized: PolicyOption{
 | 
			
		||||
				ChunkSize: 25 << 20, // 25MB
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
		if err := DB.Create(&defaultPolicy).Error; err != nil {
 | 
			
		||||
			util.Log().Panic("Failed to create default storage policy: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func addDefaultSettings() {
 | 
			
		||||
	for _, value := range defaultSettings {
 | 
			
		||||
		DB.Where(Setting{Name: value.Name}).Create(&value)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func addDefaultGroups() {
 | 
			
		||||
	_, err := GetGroupByID(1)
 | 
			
		||||
	// 未找到初始管理组时,则创建
 | 
			
		||||
	if gorm.IsRecordNotFoundError(err) {
 | 
			
		||||
		defaultAdminGroup := Group{
 | 
			
		||||
			Name:          "Admin",
 | 
			
		||||
			PolicyList:    []uint{1},
 | 
			
		||||
			MaxStorage:    1 * 1024 * 1024 * 1024,
 | 
			
		||||
			ShareEnabled:  true,
 | 
			
		||||
			WebDAVEnabled: true,
 | 
			
		||||
			OptionsSerialized: GroupOption{
 | 
			
		||||
				ArchiveDownload:  true,
 | 
			
		||||
				ArchiveTask:      true,
 | 
			
		||||
				ShareDownload:    true,
 | 
			
		||||
				ShareFree:        true,
 | 
			
		||||
				Aria2:            true,
 | 
			
		||||
				Relocate:         true,
 | 
			
		||||
				SourceBatchSize:  1000,
 | 
			
		||||
				Aria2BatchSize:   50,
 | 
			
		||||
				RedirectedSource: true,
 | 
			
		||||
				SelectNode:       true,
 | 
			
		||||
				AdvanceDelete:    true,
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
		if err := DB.Create(&defaultAdminGroup).Error; err != nil {
 | 
			
		||||
			util.Log().Panic("Failed to create admin user group: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = nil
 | 
			
		||||
	_, err = GetGroupByID(2)
 | 
			
		||||
	// 未找到初始注册会员时,则创建
 | 
			
		||||
	if gorm.IsRecordNotFoundError(err) {
 | 
			
		||||
		defaultAdminGroup := Group{
 | 
			
		||||
			Name:          "User",
 | 
			
		||||
			PolicyList:    []uint{1},
 | 
			
		||||
			MaxStorage:    1 * 1024 * 1024 * 1024,
 | 
			
		||||
			ShareEnabled:  true,
 | 
			
		||||
			WebDAVEnabled: true,
 | 
			
		||||
			OptionsSerialized: GroupOption{
 | 
			
		||||
				ShareDownload:    true,
 | 
			
		||||
				SourceBatchSize:  10,
 | 
			
		||||
				Aria2BatchSize:   1,
 | 
			
		||||
				RedirectedSource: true,
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
		if err := DB.Create(&defaultAdminGroup).Error; err != nil {
 | 
			
		||||
			util.Log().Panic("Failed to create initial user group: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = nil
 | 
			
		||||
	_, err = GetGroupByID(3)
 | 
			
		||||
	// 未找到初始游客用户组时,则创建
 | 
			
		||||
	if gorm.IsRecordNotFoundError(err) {
 | 
			
		||||
		defaultAdminGroup := Group{
 | 
			
		||||
			Name:       "Anonymous",
 | 
			
		||||
			PolicyList: []uint{},
 | 
			
		||||
			Policies:   "[]",
 | 
			
		||||
			OptionsSerialized: GroupOption{
 | 
			
		||||
				ShareDownload: true,
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
		if err := DB.Create(&defaultAdminGroup).Error; err != nil {
 | 
			
		||||
			util.Log().Panic("Failed to create anonymous user group: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func addDefaultUser() {
 | 
			
		||||
	_, err := GetUserByID(1)
 | 
			
		||||
	password := util.RandStringRunes(8)
 | 
			
		||||
 | 
			
		||||
	// 未找到初始用户时,则创建
 | 
			
		||||
	if gorm.IsRecordNotFoundError(err) {
 | 
			
		||||
		defaultUser := NewUser()
 | 
			
		||||
		defaultUser.Email = "admin@cloudreve.org"
 | 
			
		||||
		defaultUser.Nick = "admin"
 | 
			
		||||
		defaultUser.Status = Active
 | 
			
		||||
		defaultUser.GroupID = 1
 | 
			
		||||
		err := defaultUser.SetPassword(password)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			util.Log().Panic("Failed to create password: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
		if err := DB.Create(&defaultUser).Error; err != nil {
 | 
			
		||||
			util.Log().Panic("Failed to create initial root user: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c := color.New(color.FgWhite).Add(color.BgBlack).Add(color.Bold)
 | 
			
		||||
		util.Log().Info("Admin user name: " + c.Sprint("admin@cloudreve.org"))
 | 
			
		||||
		util.Log().Info("Admin password: " + c.Sprint(password))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func addDefaultNode() {
 | 
			
		||||
	_, err := GetNodeByID(1)
 | 
			
		||||
 | 
			
		||||
	if gorm.IsRecordNotFoundError(err) {
 | 
			
		||||
		defaultAdminGroup := Node{
 | 
			
		||||
			Name:   "Master (Local machine)",
 | 
			
		||||
			Status: NodeActive,
 | 
			
		||||
			Type:   MasterNodeType,
 | 
			
		||||
			Aria2OptionsSerialized: Aria2Option{
 | 
			
		||||
				Interval: 10,
 | 
			
		||||
				Timeout:  10,
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
		if err := DB.Create(&defaultAdminGroup).Error; err != nil {
 | 
			
		||||
			util.Log().Panic("Failed to create initial node: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func execUpgradeScripts() {
 | 
			
		||||
	s := invoker.ListPrefix("UpgradeTo")
 | 
			
		||||
	versions := make([]*version.Version, len(s))
 | 
			
		||||
	for i, raw := range s {
 | 
			
		||||
		v, _ := version.NewVersion(strings.TrimPrefix(raw, "UpgradeTo"))
 | 
			
		||||
		versions[i] = v
 | 
			
		||||
	}
 | 
			
		||||
	sort.Sort(version.Collection(versions))
 | 
			
		||||
 | 
			
		||||
	for i := 0; i < len(versions); i++ {
 | 
			
		||||
		invoker.RunDBScript("UpgradeTo"+versions[i].String(), context.Background())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										91
									
								
								models/node.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								models/node.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,91 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"github.com/jinzhu/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Node 从机节点信息模型
 | 
			
		||||
type Node struct {
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	Status       NodeStatus // 节点状态
 | 
			
		||||
	Name         string     // 节点别名
 | 
			
		||||
	Type         ModelType  // 节点状态
 | 
			
		||||
	Server       string     // 服务器地址
 | 
			
		||||
	SlaveKey     string     `gorm:"type:text"` // 主->从 通信密钥
 | 
			
		||||
	MasterKey    string     `gorm:"type:text"` // 从->主 通信密钥
 | 
			
		||||
	Aria2Enabled bool       // 是否支持用作离线下载节点
 | 
			
		||||
	Aria2Options string     `gorm:"type:text"` // 离线下载配置
 | 
			
		||||
	Rank         int        // 负载均衡权重
 | 
			
		||||
 | 
			
		||||
	// 数据库忽略字段
 | 
			
		||||
	Aria2OptionsSerialized Aria2Option `gorm:"-"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Aria2Option 非公有的Aria2配置属性
 | 
			
		||||
type Aria2Option struct {
 | 
			
		||||
	// RPC 服务器地址
 | 
			
		||||
	Server string `json:"server,omitempty"`
 | 
			
		||||
	// RPC 密钥
 | 
			
		||||
	Token string `json:"token,omitempty"`
 | 
			
		||||
	// 临时下载目录
 | 
			
		||||
	TempPath string `json:"temp_path,omitempty"`
 | 
			
		||||
	// 附加下载配置
 | 
			
		||||
	Options string `json:"options,omitempty"`
 | 
			
		||||
	// 下载监控间隔
 | 
			
		||||
	Interval int `json:"interval,omitempty"`
 | 
			
		||||
	// RPC API 请求超时
 | 
			
		||||
	Timeout int `json:"timeout,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type NodeStatus int
 | 
			
		||||
type ModelType int
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	NodeActive NodeStatus = iota
 | 
			
		||||
	NodeSuspend
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	SlaveNodeType ModelType = iota
 | 
			
		||||
	MasterNodeType
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetNodeByID 用ID获取节点
 | 
			
		||||
func GetNodeByID(ID interface{}) (Node, error) {
 | 
			
		||||
	var node Node
 | 
			
		||||
	result := DB.First(&node, ID)
 | 
			
		||||
	return node, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetNodesByStatus 根据给定状态获取节点
 | 
			
		||||
func GetNodesByStatus(status ...NodeStatus) ([]Node, error) {
 | 
			
		||||
	var nodes []Node
 | 
			
		||||
	result := DB.Where("status in (?)", status).Find(&nodes)
 | 
			
		||||
	return nodes, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AfterFind 找到节点后的钩子
 | 
			
		||||
func (node *Node) AfterFind() (err error) {
 | 
			
		||||
	// 解析离线下载设置到 Aria2OptionsSerialized
 | 
			
		||||
	if node.Aria2Options != "" {
 | 
			
		||||
		err = json.Unmarshal([]byte(node.Aria2Options), &node.Aria2OptionsSerialized)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BeforeSave Save策略前的钩子
 | 
			
		||||
func (node *Node) BeforeSave() (err error) {
 | 
			
		||||
	optionsValue, err := json.Marshal(&node.Aria2OptionsSerialized)
 | 
			
		||||
	node.Aria2Options = string(optionsValue)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetStatus 设置节点启用状态
 | 
			
		||||
func (node *Node) SetStatus(status NodeStatus) error {
 | 
			
		||||
	node.Status = status
 | 
			
		||||
	return DB.Model(node).Updates(map[string]interface{}{
 | 
			
		||||
		"status": status,
 | 
			
		||||
	}).Error
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										59
									
								
								models/order.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								models/order.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/util"
 | 
			
		||||
	"github.com/jinzhu/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// PackOrderType 容量包订单
 | 
			
		||||
	PackOrderType = iota
 | 
			
		||||
	// GroupOrderType 用户组订单
 | 
			
		||||
	GroupOrderType
 | 
			
		||||
	// ScoreOrderType 积分充值订单
 | 
			
		||||
	ScoreOrderType
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// OrderUnpaid 未支付
 | 
			
		||||
	OrderUnpaid = iota
 | 
			
		||||
	// OrderPaid 已支付
 | 
			
		||||
	OrderPaid
 | 
			
		||||
	// OrderCanceled 已取消
 | 
			
		||||
	OrderCanceled
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Order 交易订单
 | 
			
		||||
type Order struct {
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	UserID    uint   // 创建者ID
 | 
			
		||||
	OrderNo   string `gorm:"index:order_number"` // 商户自定义订单编号
 | 
			
		||||
	Type      int    // 订单类型
 | 
			
		||||
	Method    string // 支付类型
 | 
			
		||||
	ProductID int64  // 商品ID
 | 
			
		||||
	Num       int    // 商品数量
 | 
			
		||||
	Name      string // 订单标题
 | 
			
		||||
	Price     int    // 商品单价
 | 
			
		||||
	Status    int    // 订单状态
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create 创建订单记录
 | 
			
		||||
func (order *Order) Create() (uint, error) {
 | 
			
		||||
	if err := DB.Create(order).Error; err != nil {
 | 
			
		||||
		util.Log().Warning("Failed to insert order record: %s", err)
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	return order.ID, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateStatus 更新订单状态
 | 
			
		||||
func (order *Order) UpdateStatus(status int) {
 | 
			
		||||
	DB.Model(order).Update("status", status)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetOrderByNo 根据商户订单号查询订单
 | 
			
		||||
func GetOrderByNo(id string) (*Order, error) {
 | 
			
		||||
	var order Order
 | 
			
		||||
	err := DB.Where("order_no = ?", id).First(&order).Error
 | 
			
		||||
	return &order, err
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										267
									
								
								models/policy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										267
									
								
								models/policy.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,267 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/gob"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gofrs/uuid"
 | 
			
		||||
	"github.com/samber/lo"
 | 
			
		||||
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/cache"
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/util"
 | 
			
		||||
	"github.com/jinzhu/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Policy 存储策略
 | 
			
		||||
type Policy struct {
 | 
			
		||||
	// 表字段
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	Name               string
 | 
			
		||||
	Type               string
 | 
			
		||||
	Server             string
 | 
			
		||||
	BucketName         string
 | 
			
		||||
	IsPrivate          bool
 | 
			
		||||
	BaseURL            string
 | 
			
		||||
	AccessKey          string `gorm:"type:text"`
 | 
			
		||||
	SecretKey          string `gorm:"type:text"`
 | 
			
		||||
	MaxSize            uint64
 | 
			
		||||
	AutoRename         bool
 | 
			
		||||
	DirNameRule        string
 | 
			
		||||
	FileNameRule       string
 | 
			
		||||
	IsOriginLinkEnable bool
 | 
			
		||||
	Options            string `gorm:"type:text"`
 | 
			
		||||
 | 
			
		||||
	// 数据库忽略字段
 | 
			
		||||
	OptionsSerialized PolicyOption `gorm:"-"`
 | 
			
		||||
	MasterID          string       `gorm:"-"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PolicyOption 非公有的存储策略属性
 | 
			
		||||
type PolicyOption struct {
 | 
			
		||||
	// Upyun访问Token
 | 
			
		||||
	Token string `json:"token"`
 | 
			
		||||
	// 允许的文件扩展名
 | 
			
		||||
	FileType []string `json:"file_type"`
 | 
			
		||||
	// MimeType
 | 
			
		||||
	MimeType string `json:"mimetype"`
 | 
			
		||||
	// OauthRedirect Oauth 重定向地址
 | 
			
		||||
	OauthRedirect string `json:"od_redirect,omitempty"`
 | 
			
		||||
	// OdProxy Onedrive 反代地址
 | 
			
		||||
	OdProxy string `json:"od_proxy,omitempty"`
 | 
			
		||||
	// OdDriver OneDrive 驱动器定位符
 | 
			
		||||
	OdDriver string `json:"od_driver,omitempty"`
 | 
			
		||||
	// Region 区域代码
 | 
			
		||||
	Region string `json:"region,omitempty"`
 | 
			
		||||
	// ServerSideEndpoint 服务端请求使用的 Endpoint,为空时使用 Policy.Server 字段
 | 
			
		||||
	ServerSideEndpoint string `json:"server_side_endpoint,omitempty"`
 | 
			
		||||
	// 分片上传的分片大小
 | 
			
		||||
	ChunkSize uint64 `json:"chunk_size,omitempty"`
 | 
			
		||||
	// 分片上传时是否需要预留空间
 | 
			
		||||
	PlaceholderWithSize bool `json:"placeholder_with_size,omitempty"`
 | 
			
		||||
	// 每秒对存储端的 API 请求上限
 | 
			
		||||
	TPSLimit float64 `json:"tps_limit,omitempty"`
 | 
			
		||||
	// 每秒 API 请求爆发上限
 | 
			
		||||
	TPSLimitBurst int `json:"tps_limit_burst,omitempty"`
 | 
			
		||||
	// Set this to `true` to force the request to use path-style addressing,
 | 
			
		||||
	// i.e., `http://s3.amazonaws.com/BUCKET/KEY `
 | 
			
		||||
	S3ForcePathStyle bool `json:"s3_path_style"`
 | 
			
		||||
	// File extensions that support thumbnail generation using native policy API.
 | 
			
		||||
	ThumbExts []string `json:"thumb_exts,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// thumbSuffix 支持缩略图处理的文件扩展名
 | 
			
		||||
var thumbSuffix = map[string][]string{
 | 
			
		||||
	"local":    {},
 | 
			
		||||
	"qiniu":    {".psd", ".jpg", ".jpeg", ".png", ".gif", ".webp", ".tiff", ".bmp"},
 | 
			
		||||
	"oss":      {".jpg", ".jpeg", ".png", ".gif", ".webp", ".tiff", ".bmp"},
 | 
			
		||||
	"cos":      {".jpg", ".jpeg", ".png", ".gif", ".webp", ".tiff", ".bmp"},
 | 
			
		||||
	"upyun":    {".svg", ".jpg", ".jpeg", ".png", ".gif", ".webp", ".tiff", ".bmp"},
 | 
			
		||||
	"s3":       {},
 | 
			
		||||
	"remote":   {},
 | 
			
		||||
	"onedrive": {"*"},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	// 注册缓存用到的复杂结构
 | 
			
		||||
	gob.Register(Policy{})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetPolicyByID 用ID获取存储策略
 | 
			
		||||
func GetPolicyByID(ID interface{}) (Policy, error) {
 | 
			
		||||
	// 尝试读取缓存
 | 
			
		||||
	cacheKey := "policy_" + strconv.Itoa(int(ID.(uint)))
 | 
			
		||||
	if policy, ok := cache.Get(cacheKey); ok {
 | 
			
		||||
		return policy.(Policy), nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var policy Policy
 | 
			
		||||
	result := DB.First(&policy, ID)
 | 
			
		||||
 | 
			
		||||
	// 写入缓存
 | 
			
		||||
	if result.Error == nil {
 | 
			
		||||
		_ = cache.Set(cacheKey, policy, -1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return policy, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AfterFind 找到存储策略后的钩子
 | 
			
		||||
func (policy *Policy) AfterFind() (err error) {
 | 
			
		||||
	// 解析存储策略设置到OptionsSerialized
 | 
			
		||||
	if policy.Options != "" {
 | 
			
		||||
		err = json.Unmarshal([]byte(policy.Options), &policy.OptionsSerialized)
 | 
			
		||||
	}
 | 
			
		||||
	if policy.OptionsSerialized.FileType == nil {
 | 
			
		||||
		policy.OptionsSerialized.FileType = []string{}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BeforeSave Save策略前的钩子
 | 
			
		||||
func (policy *Policy) BeforeSave() (err error) {
 | 
			
		||||
	err = policy.SerializeOptions()
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SerializeOptions 将序列后的Option写入到数据库字段
 | 
			
		||||
func (policy *Policy) SerializeOptions() (err error) {
 | 
			
		||||
	optionsValue, err := json.Marshal(&policy.OptionsSerialized)
 | 
			
		||||
	policy.Options = string(optionsValue)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GeneratePath 生成存储文件的路径
 | 
			
		||||
func (policy *Policy) GeneratePath(uid uint, origin string) string {
 | 
			
		||||
	dirRule := policy.DirNameRule
 | 
			
		||||
	replaceTable := map[string]string{
 | 
			
		||||
		"{randomkey16}":    util.RandStringRunes(16),
 | 
			
		||||
		"{randomkey8}":     util.RandStringRunes(8),
 | 
			
		||||
		"{timestamp}":      strconv.FormatInt(time.Now().Unix(), 10),
 | 
			
		||||
		"{timestamp_nano}": strconv.FormatInt(time.Now().UnixNano(), 10),
 | 
			
		||||
		"{uid}":            strconv.Itoa(int(uid)),
 | 
			
		||||
		"{datetime}":       time.Now().Format("20060102150405"),
 | 
			
		||||
		"{date}":           time.Now().Format("20060102"),
 | 
			
		||||
		"{year}":           time.Now().Format("2006"),
 | 
			
		||||
		"{month}":          time.Now().Format("01"),
 | 
			
		||||
		"{day}":            time.Now().Format("02"),
 | 
			
		||||
		"{hour}":           time.Now().Format("15"),
 | 
			
		||||
		"{minute}":         time.Now().Format("04"),
 | 
			
		||||
		"{second}":         time.Now().Format("05"),
 | 
			
		||||
		"{path}":           origin + "/",
 | 
			
		||||
	}
 | 
			
		||||
	dirRule = util.Replace(replaceTable, dirRule)
 | 
			
		||||
	return path.Clean(dirRule)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GenerateFileName 生成存储文件名
 | 
			
		||||
func (policy *Policy) GenerateFileName(uid uint, origin string) string {
 | 
			
		||||
	// 未开启自动重命名时,直接返回原始文件名
 | 
			
		||||
	if !policy.AutoRename {
 | 
			
		||||
		return origin
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fileRule := policy.FileNameRule
 | 
			
		||||
 | 
			
		||||
	replaceTable := map[string]string{
 | 
			
		||||
		"{randomkey16}":            util.RandStringRunes(16),
 | 
			
		||||
		"{randomkey8}":             util.RandStringRunes(8),
 | 
			
		||||
		"{timestamp}":              strconv.FormatInt(time.Now().Unix(), 10),
 | 
			
		||||
		"{timestamp_nano}":         strconv.FormatInt(time.Now().UnixNano(), 10),
 | 
			
		||||
		"{uid}":                    strconv.Itoa(int(uid)),
 | 
			
		||||
		"{datetime}":               time.Now().Format("20060102150405"),
 | 
			
		||||
		"{date}":                   time.Now().Format("20060102"),
 | 
			
		||||
		"{year}":                   time.Now().Format("2006"),
 | 
			
		||||
		"{month}":                  time.Now().Format("01"),
 | 
			
		||||
		"{day}":                    time.Now().Format("02"),
 | 
			
		||||
		"{hour}":                   time.Now().Format("15"),
 | 
			
		||||
		"{minute}":                 time.Now().Format("04"),
 | 
			
		||||
		"{second}":                 time.Now().Format("05"),
 | 
			
		||||
		"{originname}":             origin,
 | 
			
		||||
		"{ext}":                    filepath.Ext(origin),
 | 
			
		||||
		"{originname_without_ext}": strings.TrimSuffix(origin, filepath.Ext(origin)),
 | 
			
		||||
		"{uuid}":                   uuid.Must(uuid.NewV4()).String(),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fileRule = util.Replace(replaceTable, fileRule)
 | 
			
		||||
	return fileRule
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsThumbExist 给定文件名,返回此存储策略下是否可能存在缩略图
 | 
			
		||||
func (policy *Policy) IsThumbExist(name string) bool {
 | 
			
		||||
	if list, ok := thumbSuffix[policy.Type]; ok {
 | 
			
		||||
		if len(list) == 1 && list[0] == "*" {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
		return util.ContainsString(list, strings.ToLower(filepath.Ext(name)))
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsDirectlyPreview 返回此策略下文件是否可以直接预览(不需要重定向)
 | 
			
		||||
func (policy *Policy) IsDirectlyPreview() bool {
 | 
			
		||||
	return policy.Type == "local"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsTransitUpload 返回此策略上传给定size文件时是否需要服务端中转
 | 
			
		||||
func (policy *Policy) IsTransitUpload(size uint64) bool {
 | 
			
		||||
	return policy.Type == "local"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsThumbGenerateNeeded 返回此策略是否需要在上传后生成缩略图
 | 
			
		||||
func (policy *Policy) IsThumbGenerateNeeded() bool {
 | 
			
		||||
	return policy.Type == "local"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsUploadPlaceholderWithSize 返回此策略创建上传会话时是否需要预留空间
 | 
			
		||||
func (policy *Policy) IsUploadPlaceholderWithSize() bool {
 | 
			
		||||
	if policy.Type == "remote" {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if util.ContainsString([]string{"onedrive", "oss", "qiniu", "cos", "s3"}, policy.Type) {
 | 
			
		||||
		return policy.OptionsSerialized.PlaceholderWithSize
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CanStructureBeListed 返回存储策略是否能被前台列物理目录
 | 
			
		||||
func (policy *Policy) CanStructureBeListed() bool {
 | 
			
		||||
	return policy.Type != "local" && policy.Type != "remote"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SaveAndClearCache 更新并清理缓存
 | 
			
		||||
func (policy *Policy) SaveAndClearCache() error {
 | 
			
		||||
	err := DB.Save(policy).Error
 | 
			
		||||
	policy.ClearCache()
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SaveAndClearCache 更新并清理缓存
 | 
			
		||||
func (policy *Policy) UpdateAccessKeyAndClearCache(s string) error {
 | 
			
		||||
	err := DB.Model(policy).UpdateColumn("access_key", s).Error
 | 
			
		||||
	policy.ClearCache()
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ClearCache 清空policy缓存
 | 
			
		||||
func (policy *Policy) ClearCache() {
 | 
			
		||||
	cache.Deletes([]string{strconv.FormatUint(uint64(policy.ID), 10)}, "policy_")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CouldProxyThumb return if proxy thumbs is allowed for this policy.
 | 
			
		||||
func (policy *Policy) CouldProxyThumb() bool {
 | 
			
		||||
	if policy.Type == "local" || !IsTrueVal(GetSettingByName("thumb_proxy_enabled")) {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	allowed := make([]uint, 0)
 | 
			
		||||
	_ = json.Unmarshal([]byte(GetSettingByName("thumb_proxy_policy")), &allowed)
 | 
			
		||||
	return lo.Contains[uint](allowed, policy.ID)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								models/redeem.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								models/redeem.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import "github.com/jinzhu/gorm"
 | 
			
		||||
 | 
			
		||||
// Redeem 兑换码
 | 
			
		||||
type Redeem struct {
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	Type      int    // 订单类型
 | 
			
		||||
	ProductID int64  // 商品ID
 | 
			
		||||
	Num       int    // 商品数量
 | 
			
		||||
	Code      string `gorm:"size:64,index:redeem_code"` // 兑换码
 | 
			
		||||
	Used      bool   // 是否已被使用
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetAvailableRedeem 根据code查找可用兑换码
 | 
			
		||||
func GetAvailableRedeem(code string) (*Redeem, error) {
 | 
			
		||||
	redeem := &Redeem{}
 | 
			
		||||
	result := DB.Where("code = ? and used = ?", code, false).First(redeem)
 | 
			
		||||
	return redeem, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Use 设定为已使用状态
 | 
			
		||||
func (redeem *Redeem) Use() {
 | 
			
		||||
	DB.Model(redeem).Updates(map[string]interface{}{
 | 
			
		||||
		"used": true,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								models/report.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								models/report.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/jinzhu/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Report 举报模型
 | 
			
		||||
type Report struct {
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	ShareID     uint   `gorm:"index:share_id"` // 对应分享ID
 | 
			
		||||
	Reason      int    // 举报原因
 | 
			
		||||
	Description string // 补充描述
 | 
			
		||||
 | 
			
		||||
	// 关联模型
 | 
			
		||||
	Share Share `gorm:"save_associations:false:false"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create 创建举报
 | 
			
		||||
func (report *Report) Create() error {
 | 
			
		||||
	return DB.Create(report).Error
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								models/scripts/init.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								models/scripts/init.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
package scripts
 | 
			
		||||
 | 
			
		||||
import "github.com/cloudreve/Cloudreve/v3/models/scripts/invoker"
 | 
			
		||||
 | 
			
		||||
func Init() {
 | 
			
		||||
	invoker.Register("ResetAdminPassword", ResetAdminPassword(0))
 | 
			
		||||
	invoker.Register("CalibrateUserStorage", UserStorageCalibration(0))
 | 
			
		||||
	invoker.Register("OSSToPlus", UpgradeToPro(0))
 | 
			
		||||
	invoker.Register("UpgradeTo3.4.0", UpgradeTo340(0))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										38
									
								
								models/scripts/invoker/invoker.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								models/scripts/invoker/invoker.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
package invoker
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/util"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type DBScript interface {
 | 
			
		||||
	Run(ctx context.Context)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var availableScripts = make(map[string]DBScript)
 | 
			
		||||
 | 
			
		||||
func RunDBScript(name string, ctx context.Context) error {
 | 
			
		||||
	if script, ok := availableScripts[name]; ok {
 | 
			
		||||
		util.Log().Info("Start executing database script %q.", name)
 | 
			
		||||
		script.Run(ctx)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return fmt.Errorf("Database script %q not exist.", name)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Register(name string, script DBScript) {
 | 
			
		||||
	availableScripts[name] = script
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ListPrefix(prefix string) []string {
 | 
			
		||||
	var scripts []string
 | 
			
		||||
	for name := range availableScripts {
 | 
			
		||||
		if strings.HasPrefix(name, prefix) {
 | 
			
		||||
			scripts = append(scripts, name)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return scripts
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										31
									
								
								models/scripts/reset.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								models/scripts/reset.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
package scripts
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	model "github.com/cloudreve/Cloudreve/v3/models"
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/util"
 | 
			
		||||
	"github.com/fatih/color"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ResetAdminPassword int
 | 
			
		||||
 | 
			
		||||
// Run 运行脚本从社区版升级至 Pro 版
 | 
			
		||||
func (script ResetAdminPassword) Run(ctx context.Context) {
 | 
			
		||||
	// 查找用户
 | 
			
		||||
	user, err := model.GetUserByID(1)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		util.Log().Panic("Initial admin user not exist: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 生成密码
 | 
			
		||||
	password := util.RandStringRunes(8)
 | 
			
		||||
 | 
			
		||||
	// 更改为新密码
 | 
			
		||||
	user.SetPassword(password)
 | 
			
		||||
	if err := user.Update(map[string]interface{}{"password": user.Password}); err != nil {
 | 
			
		||||
		util.Log().Panic("Failed to update password: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c := color.New(color.FgWhite).Add(color.BgBlack).Add(color.Bold)
 | 
			
		||||
	util.Log().Info("Initial admin user password changed to:" + c.Sprint(password))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										33
									
								
								models/scripts/storage.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								models/scripts/storage.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
package scripts
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	model "github.com/cloudreve/Cloudreve/v3/models"
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type UserStorageCalibration int
 | 
			
		||||
 | 
			
		||||
type storageResult struct {
 | 
			
		||||
	Total uint64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Run 运行脚本校准所有用户容量
 | 
			
		||||
func (script UserStorageCalibration) Run(ctx context.Context) {
 | 
			
		||||
	// 列出所有用户
 | 
			
		||||
	var res []model.User
 | 
			
		||||
	model.DB.Model(&model.User{}).Find(&res)
 | 
			
		||||
 | 
			
		||||
	// 逐个检查容量
 | 
			
		||||
	for _, user := range res {
 | 
			
		||||
		// 计算正确的容量
 | 
			
		||||
		var total storageResult
 | 
			
		||||
		model.DB.Model(&model.File{}).Where("user_id = ?", user.ID).Select("sum(size) as total").Scan(&total)
 | 
			
		||||
		// 更新用户的容量
 | 
			
		||||
		if user.Storage != total.Total {
 | 
			
		||||
			util.Log().Info("Calibrate used storage for user %q, from %d to %d.", user.Email,
 | 
			
		||||
				user.Storage, total.Total)
 | 
			
		||||
		}
 | 
			
		||||
		model.DB.Model(&user).Update("storage", total.Total)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								models/scripts/upgrade-pro.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								models/scripts/upgrade-pro.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
package scripts
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	model "github.com/cloudreve/Cloudreve/v3/models"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type UpgradeToPro int
 | 
			
		||||
 | 
			
		||||
// Run 运行脚本从社区版升级至 Pro 版
 | 
			
		||||
func (script UpgradeToPro) Run(ctx context.Context) {
 | 
			
		||||
	// folder.PolicyID 字段设为 0
 | 
			
		||||
	model.DB.Model(model.Folder{}).UpdateColumn("policy_id", 0)
 | 
			
		||||
	// shares.Score 字段设为0
 | 
			
		||||
	model.DB.Model(model.Share{}).UpdateColumn("score", 0)
 | 
			
		||||
	// user 表相关初始字段
 | 
			
		||||
	model.DB.Model(model.User{}).Updates(map[string]interface{}{
 | 
			
		||||
		"score":             0,
 | 
			
		||||
		"previous_group_id": 0,
 | 
			
		||||
		"open_id":           "",
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										43
									
								
								models/scripts/upgrade.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								models/scripts/upgrade.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
package scripts
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	model "github.com/cloudreve/Cloudreve/v3/models"
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/util"
 | 
			
		||||
	"strconv"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type UpgradeTo340 int
 | 
			
		||||
 | 
			
		||||
// Run upgrade from older version to 3.4.0
 | 
			
		||||
func (script UpgradeTo340) Run(ctx context.Context) {
 | 
			
		||||
	// 取回老版本 aria2 设定
 | 
			
		||||
	old := model.GetSettingByType([]string{"aria2"})
 | 
			
		||||
	if len(old) == 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 写入到新版本的节点设定
 | 
			
		||||
	n, err := model.GetNodeByID(1)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		util.Log().Error("找不到主机节点, %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	n.Aria2Enabled = old["aria2_rpcurl"] != ""
 | 
			
		||||
	n.Aria2OptionsSerialized.Options = old["aria2_options"]
 | 
			
		||||
	n.Aria2OptionsSerialized.Server = old["aria2_rpcurl"]
 | 
			
		||||
 | 
			
		||||
	interval, err := strconv.Atoi(old["aria2_interval"])
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		interval = 10
 | 
			
		||||
	}
 | 
			
		||||
	n.Aria2OptionsSerialized.Interval = interval
 | 
			
		||||
	n.Aria2OptionsSerialized.TempPath = old["aria2_temp_path"]
 | 
			
		||||
	n.Aria2OptionsSerialized.Token = old["aria2_token"]
 | 
			
		||||
	if err := model.DB.Save(&n).Error; err != nil {
 | 
			
		||||
		util.Log().Error("无法保存主机节点 Aria2 配置信息, %s", err)
 | 
			
		||||
	} else {
 | 
			
		||||
		model.DB.Where("type = ?", "aria2").Delete(model.Setting{})
 | 
			
		||||
		util.Log().Info("Aria2 配置信息已成功迁移至 3.4.0+ 版本的模式")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										110
									
								
								models/setting.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								models/setting.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,110 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/cache"
 | 
			
		||||
	"github.com/jinzhu/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Setting 系统设置模型
 | 
			
		||||
type Setting struct {
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	Type  string `gorm:"not null"`
 | 
			
		||||
	Name  string `gorm:"unique;not null;index:setting_key"`
 | 
			
		||||
	Value string `gorm:"size:65535"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsTrueVal 返回设置的值是否为真
 | 
			
		||||
func IsTrueVal(val string) bool {
 | 
			
		||||
	return val == "1" || val == "true"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSettingByName 用 Name 获取设置值
 | 
			
		||||
func GetSettingByName(name string) string {
 | 
			
		||||
	return GetSettingByNameFromTx(DB, name)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSettingByNameFromTx 用 Name 获取设置值,使用事务
 | 
			
		||||
func GetSettingByNameFromTx(tx *gorm.DB, name string) string {
 | 
			
		||||
	var setting Setting
 | 
			
		||||
 | 
			
		||||
	// 优先从缓存中查找
 | 
			
		||||
	cacheKey := "setting_" + name
 | 
			
		||||
	if optionValue, ok := cache.Get(cacheKey); ok {
 | 
			
		||||
		return optionValue.(string)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 尝试数据库中查找
 | 
			
		||||
	if tx == nil {
 | 
			
		||||
		tx = DB
 | 
			
		||||
		if tx == nil {
 | 
			
		||||
			return ""
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	result := tx.Where("name = ?", name).First(&setting)
 | 
			
		||||
	if result.Error == nil {
 | 
			
		||||
		_ = cache.Set(cacheKey, setting.Value, -1)
 | 
			
		||||
		return setting.Value
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSettingByNameWithDefault 用 Name 获取设置值, 取不到时使用缺省值
 | 
			
		||||
func GetSettingByNameWithDefault(name, fallback string) string {
 | 
			
		||||
	res := GetSettingByName(name)
 | 
			
		||||
	if res == "" {
 | 
			
		||||
		return fallback
 | 
			
		||||
	}
 | 
			
		||||
	return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSettingByNames 用多个 Name 获取设置值
 | 
			
		||||
func GetSettingByNames(names ...string) map[string]string {
 | 
			
		||||
	var queryRes []Setting
 | 
			
		||||
	res, miss := cache.GetSettings(names, "setting_")
 | 
			
		||||
 | 
			
		||||
	if len(miss) > 0 {
 | 
			
		||||
		DB.Where("name IN (?)", miss).Find(&queryRes)
 | 
			
		||||
		for _, setting := range queryRes {
 | 
			
		||||
			res[setting.Name] = setting.Value
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_ = cache.SetSettings(res, "setting_")
 | 
			
		||||
	return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSettingByType 获取一个或多个分组的所有设置值
 | 
			
		||||
func GetSettingByType(types []string) map[string]string {
 | 
			
		||||
	var queryRes []Setting
 | 
			
		||||
	res := make(map[string]string)
 | 
			
		||||
 | 
			
		||||
	DB.Where("type IN (?)", types).Find(&queryRes)
 | 
			
		||||
	for _, setting := range queryRes {
 | 
			
		||||
		res[setting.Name] = setting.Value
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSiteURL 获取站点地址
 | 
			
		||||
func GetSiteURL() *url.URL {
 | 
			
		||||
	base, err := url.Parse(GetSettingByName("siteURL"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		base, _ = url.Parse("https://cloudreve.org")
 | 
			
		||||
	}
 | 
			
		||||
	return base
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetIntSetting 获取整形设置值,如果转换失败则返回默认值defaultVal
 | 
			
		||||
func GetIntSetting(key string, defaultVal int) int {
 | 
			
		||||
	res, err := strconv.Atoi(GetSettingByName(key))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return defaultVal
 | 
			
		||||
	}
 | 
			
		||||
	return res
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										280
									
								
								models/share.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										280
									
								
								models/share.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,280 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"math"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/cache"
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/util"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/jinzhu/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ErrInsufficientCredit = errors.New("积分不足")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Share 分享模型
 | 
			
		||||
type Share struct {
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	Password        string     // 分享密码,空值为非加密分享
 | 
			
		||||
	IsDir           bool       // 原始资源是否为目录
 | 
			
		||||
	UserID          uint       // 创建用户ID
 | 
			
		||||
	SourceID        uint       // 原始资源ID
 | 
			
		||||
	Views           int        // 浏览数
 | 
			
		||||
	Downloads       int        // 下载数
 | 
			
		||||
	RemainDownloads int        // 剩余下载配额,负值标识无限制
 | 
			
		||||
	Expires         *time.Time // 过期时间,空值表示无过期时间
 | 
			
		||||
	Score           int        // 每人次下载扣除积分
 | 
			
		||||
	PreviewEnabled  bool       // 是否允许直接预览
 | 
			
		||||
	SourceName      string     `gorm:"index:source"` // 用于搜索的字段
 | 
			
		||||
 | 
			
		||||
	// 数据库忽略字段
 | 
			
		||||
	User   User   `gorm:"PRELOAD:false,association_autoupdate:false"`
 | 
			
		||||
	File   File   `gorm:"PRELOAD:false,association_autoupdate:false"`
 | 
			
		||||
	Folder Folder `gorm:"PRELOAD:false,association_autoupdate:false"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create 创建分享
 | 
			
		||||
func (share *Share) Create() (uint, error) {
 | 
			
		||||
	if err := DB.Create(share).Error; err != nil {
 | 
			
		||||
		util.Log().Warning("Failed to insert share record: %s", err)
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	return share.ID, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetShareByHashID 根据HashID查找分享
 | 
			
		||||
func GetShareByHashID(hashID string) *Share {
 | 
			
		||||
	id, err := hashid.DecodeHashID(hashID, hashid.ShareID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	var share Share
 | 
			
		||||
	result := DB.First(&share, id)
 | 
			
		||||
	if result.Error != nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &share
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsAvailable 返回此分享是否可用(是否过期)
 | 
			
		||||
func (share *Share) IsAvailable() bool {
 | 
			
		||||
	if share.RemainDownloads == 0 {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	if share.Expires != nil && time.Now().After(*share.Expires) {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查创建者状态
 | 
			
		||||
	if share.Creator().Status != Active {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查源对象是否存在
 | 
			
		||||
	var sourceID uint
 | 
			
		||||
	if share.IsDir {
 | 
			
		||||
		folder := share.SourceFolder()
 | 
			
		||||
		sourceID = folder.ID
 | 
			
		||||
	} else {
 | 
			
		||||
		file := share.SourceFile()
 | 
			
		||||
		sourceID = file.ID
 | 
			
		||||
	}
 | 
			
		||||
	if sourceID == 0 {
 | 
			
		||||
		// TODO 是否要在这里删除这个无效分享?
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Creator 获取分享的创建者
 | 
			
		||||
func (share *Share) Creator() *User {
 | 
			
		||||
	if share.User.ID == 0 {
 | 
			
		||||
		share.User, _ = GetUserByID(share.UserID)
 | 
			
		||||
	}
 | 
			
		||||
	return &share.User
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Source 返回源对象
 | 
			
		||||
func (share *Share) Source() interface{} {
 | 
			
		||||
	if share.IsDir {
 | 
			
		||||
		return share.SourceFolder()
 | 
			
		||||
	}
 | 
			
		||||
	return share.SourceFile()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SourceFolder 获取源目录
 | 
			
		||||
func (share *Share) SourceFolder() *Folder {
 | 
			
		||||
	if share.Folder.ID == 0 {
 | 
			
		||||
		folders, _ := GetFoldersByIDs([]uint{share.SourceID}, share.UserID)
 | 
			
		||||
		if len(folders) > 0 {
 | 
			
		||||
			share.Folder = folders[0]
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return &share.Folder
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SourceFile 获取源文件
 | 
			
		||||
func (share *Share) SourceFile() *File {
 | 
			
		||||
	if share.File.ID == 0 {
 | 
			
		||||
		files, _ := GetFilesByIDs([]uint{share.SourceID}, share.UserID)
 | 
			
		||||
		if len(files) > 0 {
 | 
			
		||||
			share.File = files[0]
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return &share.File
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CanBeDownloadBy 返回此分享是否可以被给定用户下载
 | 
			
		||||
func (share *Share) CanBeDownloadBy(user *User) error {
 | 
			
		||||
	// 用户组权限
 | 
			
		||||
	if !user.Group.OptionsSerialized.ShareDownload {
 | 
			
		||||
		if user.IsAnonymous() {
 | 
			
		||||
			return errors.New("you must login to download")
 | 
			
		||||
		}
 | 
			
		||||
		return errors.New("your group has no permission to download")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 需要积分但未登录
 | 
			
		||||
	if share.Score > 0 && user.IsAnonymous() {
 | 
			
		||||
		return errors.New("you must login to download")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WasDownloadedBy 返回分享是否已被用户下载过
 | 
			
		||||
func (share *Share) WasDownloadedBy(user *User, c *gin.Context) (exist bool) {
 | 
			
		||||
	if user.IsAnonymous() {
 | 
			
		||||
		exist = util.GetSession(c, fmt.Sprintf("share_%d_%d", share.ID, user.ID)) != nil
 | 
			
		||||
	} else {
 | 
			
		||||
		_, exist = cache.Get(fmt.Sprintf("share_%d_%d", share.ID, user.ID))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return exist
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DownloadBy 增加下载次数、检查积分等,匿名用户不会缓存
 | 
			
		||||
func (share *Share) DownloadBy(user *User, c *gin.Context) error {
 | 
			
		||||
	if !share.WasDownloadedBy(user, c) {
 | 
			
		||||
		if err := share.Purchase(user); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		share.Downloaded()
 | 
			
		||||
		if !user.IsAnonymous() {
 | 
			
		||||
			cache.Set(fmt.Sprintf("share_%d_%d", share.ID, user.ID), true,
 | 
			
		||||
				GetIntSetting("share_download_session_timeout", 2073600))
 | 
			
		||||
		} else {
 | 
			
		||||
			util.SetSession(c, map[string]interface{}{fmt.Sprintf("share_%d_%d", share.ID, user.ID): true})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Purchase 使用积分购买分享
 | 
			
		||||
func (share *Share) Purchase(user *User) error {
 | 
			
		||||
	// 不需要付积分
 | 
			
		||||
	if share.Score == 0 || user.Group.OptionsSerialized.ShareFree || user.ID == share.UserID {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ok := user.PayScore(share.Score)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return ErrInsufficientCredit
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	scoreRate := GetIntSetting("share_score_rate", 100)
 | 
			
		||||
	gainedScore := int(math.Ceil(float64(share.Score*scoreRate) / 100))
 | 
			
		||||
	share.Creator().AddScore(gainedScore)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Viewed 增加访问次数
 | 
			
		||||
func (share *Share) Viewed() {
 | 
			
		||||
	share.Views++
 | 
			
		||||
	DB.Model(share).UpdateColumn("views", gorm.Expr("views + ?", 1))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Downloaded 增加下载次数
 | 
			
		||||
func (share *Share) Downloaded() {
 | 
			
		||||
	share.Downloads++
 | 
			
		||||
	if share.RemainDownloads > 0 {
 | 
			
		||||
		share.RemainDownloads--
 | 
			
		||||
	}
 | 
			
		||||
	DB.Model(share).Updates(map[string]interface{}{
 | 
			
		||||
		"downloads":        share.Downloads,
 | 
			
		||||
		"remain_downloads": share.RemainDownloads,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Update 更新分享属性
 | 
			
		||||
func (share *Share) Update(props map[string]interface{}) error {
 | 
			
		||||
	return DB.Model(share).Updates(props).Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Delete 删除分享
 | 
			
		||||
func (share *Share) Delete() error {
 | 
			
		||||
	return DB.Model(share).Delete(share).Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteShareBySourceIDs 根据原始资源类型和ID删除文件
 | 
			
		||||
func DeleteShareBySourceIDs(sources []uint, isDir bool) error {
 | 
			
		||||
	return DB.Where("source_id in (?) and is_dir = ?", sources, isDir).Delete(&Share{}).Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ListShares 列出UID下的分享
 | 
			
		||||
func ListShares(uid uint, page, pageSize int, order string, publicOnly bool) ([]Share, int) {
 | 
			
		||||
	var (
 | 
			
		||||
		shares []Share
 | 
			
		||||
		total  int
 | 
			
		||||
	)
 | 
			
		||||
	dbChain := DB
 | 
			
		||||
	dbChain = dbChain.Where("user_id = ?", uid)
 | 
			
		||||
	if publicOnly {
 | 
			
		||||
		dbChain = dbChain.Where("password = ?", "")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 计算总数用于分页
 | 
			
		||||
	dbChain.Model(&Share{}).Count(&total)
 | 
			
		||||
 | 
			
		||||
	// 查询记录
 | 
			
		||||
	dbChain.Limit(pageSize).Offset((page - 1) * pageSize).Order(order).Find(&shares)
 | 
			
		||||
	return shares, total
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SearchShares 根据关键字搜索分享
 | 
			
		||||
func SearchShares(page, pageSize int, order, keywords string) ([]Share, int) {
 | 
			
		||||
	var (
 | 
			
		||||
		shares []Share
 | 
			
		||||
		total  int
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	keywordList := strings.Split(keywords, " ")
 | 
			
		||||
	availableList := make([]string, 0, len(keywordList))
 | 
			
		||||
	for i := 0; i < len(keywordList); i++ {
 | 
			
		||||
		if len(keywordList[i]) > 0 {
 | 
			
		||||
			availableList = append(availableList, keywordList[i])
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if len(availableList) == 0 {
 | 
			
		||||
		return shares, 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dbChain := DB
 | 
			
		||||
	dbChain = dbChain.Where("password = ? and remain_downloads <> 0 and (expires is NULL or expires > ?) and source_name like ?", "", time.Now(), "%"+strings.Join(availableList, "%")+"%")
 | 
			
		||||
 | 
			
		||||
	// 计算总数用于分页
 | 
			
		||||
	dbChain.Model(&Share{}).Count(&total)
 | 
			
		||||
 | 
			
		||||
	// 查询记录
 | 
			
		||||
	dbChain.Limit(pageSize).Offset((page - 1) * pageSize).Order(order).Find(&shares)
 | 
			
		||||
	return shares, total
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								models/source_link.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								models/source_link.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
 | 
			
		||||
	"github.com/jinzhu/gorm"
 | 
			
		||||
	"net/url"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// SourceLink represent a shared file source link
 | 
			
		||||
type SourceLink struct {
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	FileID    uint   // corresponding file ID
 | 
			
		||||
	Name      string // name of the file while creating the source link, for annotation
 | 
			
		||||
	Downloads int    // 下载数
 | 
			
		||||
 | 
			
		||||
	// 关联模型
 | 
			
		||||
	File File `gorm:"save_associations:false:false"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Link gets the URL of a SourceLink
 | 
			
		||||
func (s *SourceLink) Link() (string, error) {
 | 
			
		||||
	baseURL := GetSiteURL()
 | 
			
		||||
	linkPath, err := url.Parse(fmt.Sprintf("/f/%s/%s", hashid.HashID(s.ID, hashid.SourceLinkID), s.File.Name))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return baseURL.ResolveReference(linkPath).String(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetTasksByID queries source link based on ID
 | 
			
		||||
func GetSourceLinkByID(id interface{}) (*SourceLink, error) {
 | 
			
		||||
	link := &SourceLink{}
 | 
			
		||||
	result := DB.Where("id = ?", id).First(link)
 | 
			
		||||
	files, _ := GetFilesByIDs([]uint{link.FileID}, 0)
 | 
			
		||||
	if len(files) > 0 {
 | 
			
		||||
		link.File = files[0]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return link, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Viewed 增加访问次数
 | 
			
		||||
func (s *SourceLink) Downloaded() {
 | 
			
		||||
	s.Downloads++
 | 
			
		||||
	DB.Model(s).UpdateColumn("downloads", gorm.Expr("downloads + ?", 1))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										91
									
								
								models/storage_pack.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								models/storage_pack.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,91 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/cache"
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/util"
 | 
			
		||||
	"github.com/jinzhu/gorm"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// StoragePack 容量包模型
 | 
			
		||||
type StoragePack struct {
 | 
			
		||||
	// 表字段
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	Name        string
 | 
			
		||||
	UserID      uint
 | 
			
		||||
	ActiveTime  *time.Time
 | 
			
		||||
	ExpiredTime *time.Time `gorm:"index:expired"`
 | 
			
		||||
	Size        uint64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create 创建容量包
 | 
			
		||||
func (pack *StoragePack) Create() (uint, error) {
 | 
			
		||||
	if err := DB.Create(pack).Error; err != nil {
 | 
			
		||||
		util.Log().Warning("Failed to insert storage pack record: %s", err)
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	return pack.ID, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetAvailablePackSize 返回给定用户当前可用的容量包总容量
 | 
			
		||||
func (user *User) GetAvailablePackSize() uint64 {
 | 
			
		||||
	var (
 | 
			
		||||
		total       uint64
 | 
			
		||||
		firstExpire *time.Time
 | 
			
		||||
		timeNow     = time.Now()
 | 
			
		||||
		ttl         int64
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// 尝试从缓存中读取
 | 
			
		||||
	cacheKey := "pack_size_" + strconv.FormatUint(uint64(user.ID), 10)
 | 
			
		||||
	if total, ok := cache.Get(cacheKey); ok {
 | 
			
		||||
		return total.(uint64)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 查找所有有效容量包
 | 
			
		||||
	packs := user.GetAvailableStoragePacks()
 | 
			
		||||
 | 
			
		||||
	// 计算总容量, 并找到其中最早的过期时间
 | 
			
		||||
	for _, v := range packs {
 | 
			
		||||
		total += v.Size
 | 
			
		||||
		if firstExpire == nil {
 | 
			
		||||
			firstExpire = v.ExpiredTime
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if v.ExpiredTime != nil && firstExpire.After(*v.ExpiredTime) {
 | 
			
		||||
			firstExpire = v.ExpiredTime
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 用最早的过期时间计算缓存TTL,并写入缓存
 | 
			
		||||
	if firstExpire != nil {
 | 
			
		||||
		ttl = firstExpire.Unix() - timeNow.Unix()
 | 
			
		||||
		if ttl > 0 {
 | 
			
		||||
			_ = cache.Set(cacheKey, total, int(ttl))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return total
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetAvailableStoragePacks 返回用户可用的容量包
 | 
			
		||||
func (user *User) GetAvailableStoragePacks() []StoragePack {
 | 
			
		||||
	var packs []StoragePack
 | 
			
		||||
	timeNow := time.Now()
 | 
			
		||||
	// 查找所有有效容量包
 | 
			
		||||
	DB.Where("expired_time > ? AND user_id = ?", timeNow, user.ID).Find(&packs)
 | 
			
		||||
	return packs
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetExpiredStoragePack 获取已过期的容量包
 | 
			
		||||
func GetExpiredStoragePack() []StoragePack {
 | 
			
		||||
	var packs []StoragePack
 | 
			
		||||
	DB.Where("expired_time < ?", time.Now()).Find(&packs)
 | 
			
		||||
	return packs
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Delete 删除容量包
 | 
			
		||||
func (pack *StoragePack) Delete() error {
 | 
			
		||||
	return DB.Delete(&pack).Error
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										53
									
								
								models/tag.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								models/tag.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/util"
 | 
			
		||||
	"github.com/jinzhu/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Tag 用户自定义标签
 | 
			
		||||
type Tag struct {
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	Name       string // 标签名
 | 
			
		||||
	Icon       string // 图标标识
 | 
			
		||||
	Color      string // 图标颜色
 | 
			
		||||
	Type       int    // 标签类型(文件分类/目录直达)
 | 
			
		||||
	Expression string `gorm:"type:text"` // 搜索表表达式/直达路径
 | 
			
		||||
	UserID     uint   // 创建者ID
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// FileTagType 文件分类标签
 | 
			
		||||
	FileTagType = iota
 | 
			
		||||
	// DirectoryLinkType 目录快捷方式标签
 | 
			
		||||
	DirectoryLinkType
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Create 创建标签记录
 | 
			
		||||
func (tag *Tag) Create() (uint, error) {
 | 
			
		||||
	if err := DB.Create(tag).Error; err != nil {
 | 
			
		||||
		util.Log().Warning("Failed to insert tag record: %s", err)
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	return tag.ID, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteTagByID 根据给定ID和用户ID删除标签
 | 
			
		||||
func DeleteTagByID(id, uid uint) error {
 | 
			
		||||
	result := DB.Where("id = ? and user_id = ?", id, uid).Delete(&Tag{})
 | 
			
		||||
	return result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetTagsByUID 根据用户ID查找标签
 | 
			
		||||
func GetTagsByUID(uid uint) ([]Tag, error) {
 | 
			
		||||
	var tag []Tag
 | 
			
		||||
	result := DB.Where("user_id = ?", uid).Find(&tag)
 | 
			
		||||
	return tag, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetTagsByID 根据ID查找标签
 | 
			
		||||
func GetTagsByID(id, uid uint) (*Tag, error) {
 | 
			
		||||
	var tag Tag
 | 
			
		||||
	result := DB.Where("user_id = ? and id = ?", uid, id).First(&tag)
 | 
			
		||||
	return &tag, result.Error
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										73
									
								
								models/task.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								models/task.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/util"
 | 
			
		||||
	"github.com/jinzhu/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Task 任务模型
 | 
			
		||||
type Task struct {
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	Status   int    // 任务状态
 | 
			
		||||
	Type     int    // 任务类型
 | 
			
		||||
	UserID   uint   // 发起者UID,0表示为系统发起
 | 
			
		||||
	Progress int    // 进度
 | 
			
		||||
	Error    string `gorm:"type:text"` // 错误信息
 | 
			
		||||
	Props    string `gorm:"type:text"` // 任务属性
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create 创建任务记录
 | 
			
		||||
func (task *Task) Create() (uint, error) {
 | 
			
		||||
	if err := DB.Create(task).Error; err != nil {
 | 
			
		||||
		util.Log().Warning("Failed to insert task record: %s", err)
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	return task.ID, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetStatus 设定任务状态
 | 
			
		||||
func (task *Task) SetStatus(status int) error {
 | 
			
		||||
	return DB.Model(task).Select("status").Updates(map[string]interface{}{"status": status}).Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetProgress 设定任务进度
 | 
			
		||||
func (task *Task) SetProgress(progress int) error {
 | 
			
		||||
	return DB.Model(task).Select("progress").Updates(map[string]interface{}{"progress": progress}).Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetError 设定错误信息
 | 
			
		||||
func (task *Task) SetError(err string) error {
 | 
			
		||||
	return DB.Model(task).Select("error").Updates(map[string]interface{}{"error": err}).Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetTasksByStatus 根据状态检索任务
 | 
			
		||||
func GetTasksByStatus(status ...int) []Task {
 | 
			
		||||
	var tasks []Task
 | 
			
		||||
	DB.Where("status in (?)", status).Find(&tasks)
 | 
			
		||||
	return tasks
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetTasksByID 根据ID检索任务
 | 
			
		||||
func GetTasksByID(id interface{}) (*Task, error) {
 | 
			
		||||
	task := &Task{}
 | 
			
		||||
	result := DB.Where("id = ?", id).First(task)
 | 
			
		||||
	return task, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ListTasks 列出用户所属的任务
 | 
			
		||||
func ListTasks(uid uint, page, pageSize int, order string) ([]Task, int) {
 | 
			
		||||
	var (
 | 
			
		||||
		tasks []Task
 | 
			
		||||
		total int
 | 
			
		||||
	)
 | 
			
		||||
	dbChain := DB
 | 
			
		||||
	dbChain = dbChain.Where("user_id = ?", uid)
 | 
			
		||||
 | 
			
		||||
	// 计算总数用于分页
 | 
			
		||||
	dbChain.Model(&Task{}).Count(&total)
 | 
			
		||||
 | 
			
		||||
	// 查询记录
 | 
			
		||||
	dbChain.Limit(pageSize).Offset((page - 1) * pageSize).Order(order).Find(&tasks)
 | 
			
		||||
 | 
			
		||||
	return tasks, total
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										429
									
								
								models/user.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										429
									
								
								models/user.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,429 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/md5"
 | 
			
		||||
	"crypto/sha1"
 | 
			
		||||
	"encoding/gob"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/util"
 | 
			
		||||
	"github.com/jinzhu/gorm"
 | 
			
		||||
	"github.com/pkg/errors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// Active 账户正常状态
 | 
			
		||||
	Active = iota
 | 
			
		||||
	// NotActivicated 未激活
 | 
			
		||||
	NotActivicated
 | 
			
		||||
	// Baned 被封禁
 | 
			
		||||
	Baned
 | 
			
		||||
	// OveruseBaned 超额使用被封禁
 | 
			
		||||
	OveruseBaned
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// User 用户模型
 | 
			
		||||
type User struct {
 | 
			
		||||
	// 表字段
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	Email           string `gorm:"type:varchar(100);unique_index"`
 | 
			
		||||
	Nick            string `gorm:"size:50"`
 | 
			
		||||
	Password        string `json:"-"`
 | 
			
		||||
	Status          int
 | 
			
		||||
	GroupID         uint
 | 
			
		||||
	Storage         uint64
 | 
			
		||||
	OpenID          string
 | 
			
		||||
	TwoFactor       string
 | 
			
		||||
	Avatar          string
 | 
			
		||||
	Options         string `json:"-" gorm:"size:4294967295"`
 | 
			
		||||
	Authn           string `gorm:"size:4294967295"`
 | 
			
		||||
	Score           int
 | 
			
		||||
	PreviousGroupID uint       // 初始用户组
 | 
			
		||||
	GroupExpires    *time.Time // 用户组过期日期
 | 
			
		||||
	NotifyDate      *time.Time // 通知超出配额时的日期
 | 
			
		||||
	Phone           string
 | 
			
		||||
 | 
			
		||||
	// 关联模型
 | 
			
		||||
	Group Group `gorm:"save_associations:false:false"`
 | 
			
		||||
 | 
			
		||||
	// 数据库忽略字段
 | 
			
		||||
	OptionsSerialized UserOption `gorm:"-"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	gob.Register(User{})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UserOption 用户个性化配置字段
 | 
			
		||||
type UserOption struct {
 | 
			
		||||
	ProfileOff      bool   `json:"profile_off,omitempty"`
 | 
			
		||||
	PreferredPolicy uint   `json:"preferred_policy,omitempty"`
 | 
			
		||||
	PreferredTheme  string `json:"preferred_theme,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Root 获取用户的根目录
 | 
			
		||||
func (user *User) Root() (*Folder, error) {
 | 
			
		||||
	var folder Folder
 | 
			
		||||
	err := DB.Where("parent_id is NULL AND owner_id = ?", user.ID).First(&folder).Error
 | 
			
		||||
	return &folder, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeductionStorage 减少用户已用容量
 | 
			
		||||
func (user *User) DeductionStorage(size uint64) bool {
 | 
			
		||||
	if size == 0 {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	if size <= user.Storage {
 | 
			
		||||
		user.Storage -= size
 | 
			
		||||
		DB.Model(user).Update("storage", gorm.Expr("storage - ?", size))
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	// 如果要减少的容量超出已用容量,则设为零
 | 
			
		||||
	user.Storage = 0
 | 
			
		||||
	DB.Model(user).Update("storage", 0)
 | 
			
		||||
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IncreaseStorage 检查并增加用户已用容量
 | 
			
		||||
func (user *User) IncreaseStorage(size uint64) bool {
 | 
			
		||||
	if size == 0 {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	if size <= user.GetRemainingCapacity() {
 | 
			
		||||
		user.Storage += size
 | 
			
		||||
		DB.Model(user).Update("storage", gorm.Expr("storage + ?", size))
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ChangeStorage 更新用户容量
 | 
			
		||||
func (user *User) ChangeStorage(tx *gorm.DB, operator string, size uint64) error {
 | 
			
		||||
	return tx.Model(user).Update("storage", gorm.Expr("storage "+operator+" ?", size)).Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PayScore 扣除积分,返回是否成功
 | 
			
		||||
func (user *User) PayScore(score int) bool {
 | 
			
		||||
	if score == 0 {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	if score <= user.Score {
 | 
			
		||||
		user.Score -= score
 | 
			
		||||
		DB.Model(user).Update("score", gorm.Expr("score - ?", score))
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AddScore 增加积分
 | 
			
		||||
func (user *User) AddScore(score int) {
 | 
			
		||||
	user.Score += score
 | 
			
		||||
	DB.Model(user).Update("score", gorm.Expr("score + ?", score))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IncreaseStorageWithoutCheck 忽略可用容量,增加用户已用容量
 | 
			
		||||
func (user *User) IncreaseStorageWithoutCheck(size uint64) {
 | 
			
		||||
	if size == 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	user.Storage += size
 | 
			
		||||
	DB.Model(user).Update("storage", gorm.Expr("storage + ?", size))
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetRemainingCapacity 获取剩余配额
 | 
			
		||||
func (user *User) GetRemainingCapacity() uint64 {
 | 
			
		||||
	total := user.Group.MaxStorage + user.GetAvailablePackSize()
 | 
			
		||||
	if total <= user.Storage {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
	return total - user.Storage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetPolicyID 获取给定目录的存储策略, 如果为 nil 则使用默认
 | 
			
		||||
func (user *User) GetPolicyID(folder *Folder) *Policy {
 | 
			
		||||
	if user.IsAnonymous() {
 | 
			
		||||
		return &Policy{Type: "anonymous"}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	defaultPolicy := uint(1)
 | 
			
		||||
	if len(user.Group.PolicyList) > 0 {
 | 
			
		||||
		defaultPolicy = user.Group.PolicyList[0]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if folder != nil {
 | 
			
		||||
		prefer := folder.PolicyID
 | 
			
		||||
		if prefer == 0 && folder.InheritPolicyID > 0 {
 | 
			
		||||
			prefer = folder.InheritPolicyID
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if prefer > 0 && util.ContainsUint(user.Group.PolicyList, prefer) {
 | 
			
		||||
			defaultPolicy = prefer
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	p, _ := GetPolicyByID(defaultPolicy)
 | 
			
		||||
	return &p
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetPolicyByPreference 在可用存储策略中优先获取 preference
 | 
			
		||||
func (user *User) GetPolicyByPreference(preference uint) *Policy {
 | 
			
		||||
	if user.IsAnonymous() {
 | 
			
		||||
		return &Policy{Type: "anonymous"}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	defaultPolicy := uint(1)
 | 
			
		||||
	if len(user.Group.PolicyList) > 0 {
 | 
			
		||||
		defaultPolicy = user.Group.PolicyList[0]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if preference != 0 {
 | 
			
		||||
		if util.ContainsUint(user.Group.PolicyList, preference) {
 | 
			
		||||
			defaultPolicy = preference
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	p, _ := GetPolicyByID(defaultPolicy)
 | 
			
		||||
	return &p
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetUserByID 用ID获取用户
 | 
			
		||||
func GetUserByID(ID interface{}) (User, error) {
 | 
			
		||||
	var user User
 | 
			
		||||
	result := DB.Set("gorm:auto_preload", true).First(&user, ID)
 | 
			
		||||
	return user, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetActiveUserByID 用ID获取可登录用户
 | 
			
		||||
func GetActiveUserByID(ID interface{}) (User, error) {
 | 
			
		||||
	var user User
 | 
			
		||||
	result := DB.Set("gorm:auto_preload", true).Where("status = ?", Active).First(&user, ID)
 | 
			
		||||
	return user, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetActiveUserByOpenID 用OpenID获取可登录用户
 | 
			
		||||
func GetActiveUserByOpenID(openid string) (User, error) {
 | 
			
		||||
	var user User
 | 
			
		||||
	result := DB.Set("gorm:auto_preload", true).Where("status = ? and open_id = ?", Active, openid).Find(&user)
 | 
			
		||||
	return user, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetUserByEmail 用Email获取用户
 | 
			
		||||
func GetUserByEmail(email string) (User, error) {
 | 
			
		||||
	var user User
 | 
			
		||||
	result := DB.Set("gorm:auto_preload", true).Where("email = ?", email).First(&user)
 | 
			
		||||
	return user, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetActiveUserByEmail 用Email获取可登录用户
 | 
			
		||||
func GetActiveUserByEmail(email string) (User, error) {
 | 
			
		||||
	var user User
 | 
			
		||||
	result := DB.Set("gorm:auto_preload", true).Where("status = ? and email = ?", Active, email).First(&user)
 | 
			
		||||
	return user, result.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewUser 返回一个新的空 User
 | 
			
		||||
func NewUser() User {
 | 
			
		||||
	options := UserOption{}
 | 
			
		||||
	return User{
 | 
			
		||||
		OptionsSerialized: options,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BeforeSave Save用户前的钩子
 | 
			
		||||
func (user *User) BeforeSave() (err error) {
 | 
			
		||||
	err = user.SerializeOptions()
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AfterCreate 创建用户后的钩子
 | 
			
		||||
func (user *User) AfterCreate(tx *gorm.DB) (err error) {
 | 
			
		||||
	// 创建用户的默认根目录
 | 
			
		||||
	defaultFolder := &Folder{
 | 
			
		||||
		Name:    "/",
 | 
			
		||||
		OwnerID: user.ID,
 | 
			
		||||
	}
 | 
			
		||||
	tx.Create(defaultFolder)
 | 
			
		||||
 | 
			
		||||
	// 创建用户初始文件记录
 | 
			
		||||
	initialFiles := GetSettingByNameFromTx(tx, "initial_files")
 | 
			
		||||
	if initialFiles != "" {
 | 
			
		||||
		initialFileIDs := make([]uint, 0)
 | 
			
		||||
		if err := json.Unmarshal([]byte(initialFiles), &initialFileIDs); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if files, err := GetFilesByIDsFromTX(tx, initialFileIDs, 0); err == nil {
 | 
			
		||||
			for _, file := range files {
 | 
			
		||||
				file.ID = 0
 | 
			
		||||
				file.UserID = user.ID
 | 
			
		||||
				file.FolderID = defaultFolder.ID
 | 
			
		||||
				user.Storage += file.Size
 | 
			
		||||
				tx.Create(&file)
 | 
			
		||||
			}
 | 
			
		||||
			tx.Save(user)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AfterFind 找到用户后的钩子
 | 
			
		||||
func (user *User) AfterFind() (err error) {
 | 
			
		||||
	// 解析用户设置到OptionsSerialized
 | 
			
		||||
	if user.Options != "" {
 | 
			
		||||
		err = json.Unmarshal([]byte(user.Options), &user.OptionsSerialized)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SerializeOptions 将序列后的Option写入到数据库字段
 | 
			
		||||
func (user *User) SerializeOptions() (err error) {
 | 
			
		||||
	optionsValue, err := json.Marshal(&user.OptionsSerialized)
 | 
			
		||||
	user.Options = string(optionsValue)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CheckPassword 根据明文校验密码
 | 
			
		||||
func (user *User) CheckPassword(password string) (bool, error) {
 | 
			
		||||
 | 
			
		||||
	// 根据存储密码拆分为 Salt 和 Digest
 | 
			
		||||
	passwordStore := strings.Split(user.Password, ":")
 | 
			
		||||
	if len(passwordStore) != 2 && len(passwordStore) != 3 {
 | 
			
		||||
		return false, errors.New("Unknown password type")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 兼容V2密码,升级后存储格式为: md5:$HASH:$SALT
 | 
			
		||||
	if len(passwordStore) == 3 {
 | 
			
		||||
		if passwordStore[0] != "md5" {
 | 
			
		||||
			return false, errors.New("Unknown password type")
 | 
			
		||||
		}
 | 
			
		||||
		hash := md5.New()
 | 
			
		||||
		_, err := hash.Write([]byte(passwordStore[2] + password))
 | 
			
		||||
		bs := hex.EncodeToString(hash.Sum(nil))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return false, err
 | 
			
		||||
		}
 | 
			
		||||
		return bs == passwordStore[1], nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//计算 Salt 和密码组合的SHA1摘要
 | 
			
		||||
	hash := sha1.New()
 | 
			
		||||
	_, err := hash.Write([]byte(password + passwordStore[0]))
 | 
			
		||||
	bs := hex.EncodeToString(hash.Sum(nil))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return bs == passwordStore[1], nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetPassword 根据给定明文设定 User 的 Password 字段
 | 
			
		||||
func (user *User) SetPassword(password string) error {
 | 
			
		||||
	//生成16位 Salt
 | 
			
		||||
	salt := util.RandStringRunes(16)
 | 
			
		||||
 | 
			
		||||
	//计算 Salt 和密码组合的SHA1摘要
 | 
			
		||||
	hash := sha1.New()
 | 
			
		||||
	_, err := hash.Write([]byte(password + salt))
 | 
			
		||||
	bs := hex.EncodeToString(hash.Sum(nil))
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//存储 Salt 值和摘要, ":"分割
 | 
			
		||||
	user.Password = salt + ":" + string(bs)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewAnonymousUser 返回一个匿名用户
 | 
			
		||||
func NewAnonymousUser() *User {
 | 
			
		||||
	user := User{}
 | 
			
		||||
	user.Group, _ = GetGroupByID(3)
 | 
			
		||||
	return &user
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsAnonymous 返回是否为未登录用户
 | 
			
		||||
func (user *User) IsAnonymous() bool {
 | 
			
		||||
	return user.ID == 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Notified 更新用户容量超额通知日期
 | 
			
		||||
func (user *User) Notified() {
 | 
			
		||||
	if user.NotifyDate == nil {
 | 
			
		||||
		timeNow := time.Now()
 | 
			
		||||
		user.NotifyDate = &timeNow
 | 
			
		||||
		DB.Model(&user).Update("notify_date", user.NotifyDate)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ClearNotified 清除用户通知标记
 | 
			
		||||
func (user *User) ClearNotified() {
 | 
			
		||||
	DB.Model(&user).Update("notify_date", nil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetStatus 设定用户状态
 | 
			
		||||
func (user *User) SetStatus(status int) {
 | 
			
		||||
	DB.Model(&user).Update("status", status)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Update 更新用户
 | 
			
		||||
func (user *User) Update(val map[string]interface{}) error {
 | 
			
		||||
	return DB.Model(user).Updates(val).Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateOptions 更新用户偏好设定
 | 
			
		||||
func (user *User) UpdateOptions() error {
 | 
			
		||||
	if err := user.SerializeOptions(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return user.Update(map[string]interface{}{"options": user.Options})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetGroupExpiredUsers 获取用户组过期的用户
 | 
			
		||||
func GetGroupExpiredUsers() []User {
 | 
			
		||||
	var users []User
 | 
			
		||||
	DB.Where("group_expires < ? and previous_group_id <> 0", time.Now()).Find(&users)
 | 
			
		||||
	return users
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetTolerantExpiredUser 获取超过宽容期的用户
 | 
			
		||||
func GetTolerantExpiredUser() []User {
 | 
			
		||||
	var users []User
 | 
			
		||||
	DB.Set("gorm:auto_preload", true).Where("notify_date < ?", time.Now().Add(
 | 
			
		||||
		time.Duration(-GetIntSetting("ban_time", 10))*time.Second),
 | 
			
		||||
	).Find(&users)
 | 
			
		||||
	return users
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GroupFallback 回退到初始用户组
 | 
			
		||||
func (user *User) GroupFallback() {
 | 
			
		||||
	if user.GroupExpires != nil && user.PreviousGroupID != 0 {
 | 
			
		||||
		user.Group.ID = user.PreviousGroupID
 | 
			
		||||
		DB.Model(&user).Updates(map[string]interface{}{
 | 
			
		||||
			"group_expires":     nil,
 | 
			
		||||
			"previous_group_id": 0,
 | 
			
		||||
			"group_id":          user.PreviousGroupID,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpgradeGroup 升级用户组
 | 
			
		||||
func (user *User) UpgradeGroup(id uint, expires *time.Time) error {
 | 
			
		||||
	user.Group.ID = id
 | 
			
		||||
	previousGroupID := user.GroupID
 | 
			
		||||
	if user.PreviousGroupID != 0 && user.GroupID == id {
 | 
			
		||||
		previousGroupID = user.PreviousGroupID
 | 
			
		||||
	}
 | 
			
		||||
	return DB.Model(&user).Updates(map[string]interface{}{
 | 
			
		||||
		"group_expires":     expires,
 | 
			
		||||
		"previous_group_id": previousGroupID,
 | 
			
		||||
		"group_id":          id,
 | 
			
		||||
	}).Error
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										79
									
								
								models/user_authn.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								models/user_authn.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,79 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"encoding/binary"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/url"
 | 
			
		||||
 | 
			
		||||
	"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
 | 
			
		||||
	"github.com/duo-labs/webauthn/webauthn"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
	`webauthn.User` 接口的实现
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
// WebAuthnID 返回用户ID
 | 
			
		||||
func (user User) WebAuthnID() []byte {
 | 
			
		||||
	bs := make([]byte, 8)
 | 
			
		||||
	binary.LittleEndian.PutUint64(bs, uint64(user.ID))
 | 
			
		||||
	return bs
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WebAuthnName 返回用户名
 | 
			
		||||
func (user User) WebAuthnName() string {
 | 
			
		||||
	return user.Email
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WebAuthnDisplayName 获得用于展示的用户名
 | 
			
		||||
func (user User) WebAuthnDisplayName() string {
 | 
			
		||||
	return user.Nick
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WebAuthnIcon 获得用户头像
 | 
			
		||||
func (user User) WebAuthnIcon() string {
 | 
			
		||||
	avatar, _ := url.Parse("/api/v3/user/avatar/" + hashid.HashID(user.ID, hashid.UserID) + "/l")
 | 
			
		||||
	base := GetSiteURL()
 | 
			
		||||
	base.Scheme = "https"
 | 
			
		||||
	return base.ResolveReference(avatar).String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WebAuthnCredentials 获得已注册的验证器凭证
 | 
			
		||||
func (user User) WebAuthnCredentials() []webauthn.Credential {
 | 
			
		||||
	var res []webauthn.Credential
 | 
			
		||||
	err := json.Unmarshal([]byte(user.Authn), &res)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Println(err)
 | 
			
		||||
	}
 | 
			
		||||
	return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RegisterAuthn 添加新的验证器
 | 
			
		||||
func (user *User) RegisterAuthn(credential *webauthn.Credential) error {
 | 
			
		||||
	exists := user.WebAuthnCredentials()
 | 
			
		||||
	exists = append(exists, *credential)
 | 
			
		||||
	res, err := json.Marshal(exists)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return DB.Model(user).Update("authn", string(res)).Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RemoveAuthn 删除验证器
 | 
			
		||||
func (user *User) RemoveAuthn(id string) {
 | 
			
		||||
	exists := user.WebAuthnCredentials()
 | 
			
		||||
	for i := 0; i < len(exists); i++ {
 | 
			
		||||
		idEncoded := base64.StdEncoding.EncodeToString(exists[i].ID)
 | 
			
		||||
		if idEncoded == id {
 | 
			
		||||
			exists[len(exists)-1], exists[i] = exists[i], exists[len(exists)-1]
 | 
			
		||||
			exists = exists[:len(exists)-1]
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res, _ := json.Marshal(exists)
 | 
			
		||||
	DB.Model(user).Update("authn", string(res))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										53
									
								
								models/webdav.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								models/webdav.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/jinzhu/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Webdav 应用账户
 | 
			
		||||
type Webdav struct {
 | 
			
		||||
	gorm.Model
 | 
			
		||||
	Name     string // 应用名称
 | 
			
		||||
	Password string `gorm:"unique_index:password_only_on"` // 应用密码
 | 
			
		||||
	UserID   uint   `gorm:"unique_index:password_only_on"` // 用户ID
 | 
			
		||||
	Root     string `gorm:"type:text"`                     // 根目录
 | 
			
		||||
	Readonly bool   `gorm:"type:bool"`                     // 是否只读
 | 
			
		||||
	UseProxy bool   `gorm:"type:bool"`                     // 是否进行反代
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create 创建账户
 | 
			
		||||
func (webdav *Webdav) Create() (uint, error) {
 | 
			
		||||
	if err := DB.Create(webdav).Error; err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	return webdav.ID, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetWebdavByPassword 根据密码和用户查找Webdav应用
 | 
			
		||||
func GetWebdavByPassword(password string, uid uint) (*Webdav, error) {
 | 
			
		||||
	webdav := &Webdav{}
 | 
			
		||||
	res := DB.Where("user_id = ? and password = ?", uid, password).First(webdav)
 | 
			
		||||
	return webdav, res.Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ListWebDAVAccounts 列出用户的所有账号
 | 
			
		||||
func ListWebDAVAccounts(uid uint) []Webdav {
 | 
			
		||||
	var accounts []Webdav
 | 
			
		||||
	DB.Where("user_id = ?", uid).Order("created_at desc").Find(&accounts)
 | 
			
		||||
	return accounts
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteWebDAVAccountByID 根据账户ID和UID删除账户
 | 
			
		||||
func DeleteWebDAVAccountByID(id, uid uint) {
 | 
			
		||||
	DB.Where("user_id = ? and id = ?", uid, id).Delete(&Webdav{})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateWebDAVAccountByID 根据账户ID和UID更新账户
 | 
			
		||||
func UpdateWebDAVAccountByID(id, uid uint, updates map[string]interface{}) {
 | 
			
		||||
	DB.Model(&Webdav{Model: gorm.Model{ID: id}, UserID: uid}).Updates(updates)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateWebDAVAccountReadonlyByID 根据账户ID和UID更新账户的只读性
 | 
			
		||||
func UpdateWebDAVAccountReadonlyByID(id, uid uint, readonly bool) {
 | 
			
		||||
	DB.Model(&Webdav{Model: gorm.Model{ID: id}, UserID: uid}).UpdateColumn("readonly", readonly)
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user