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