feat: add direct s3 upload flow

This commit is contained in:
2026-05-13 11:24:13 -07:00
parent f6911965dc
commit 4fa9fcd232
16 changed files with 773 additions and 106 deletions
+58
View File
@@ -0,0 +1,58 @@
syntax = "v1"
info (
title: "File API"
desc: "API for ppanel file upload"
author: "Codex"
email: "codex@openai.com"
version: "0.0.1"
)
import "../types.api"
type (
FileUploadInitRequest {
BizType string `json:"biz_type" validate:"required"`
FileName string `json:"file_name" validate:"required"`
ContentType string `json:"content_type" validate:"required"`
Size int64 `json:"size" validate:"required"`
Sha256 string `json:"sha256"`
}
FileUploadInitResponse {
FileId string `json:"file_id"`
ObjectKey string `json:"object_key"`
UploadURL string `json:"upload_url"`
Method string `json:"method"`
Headers map[string]string `json:"headers"`
ExpiredAt int64 `json:"expired_at"`
}
FileUploadCompleteRequest {
FileId string `json:"file_id" validate:"required"`
}
FileUploadCompleteResponse {
FileId string `json:"file_id"`
ObjectKey string `json:"object_key"`
Size int64 `json:"size"`
ContentType string `json:"content_type"`
Etag string `json:"etag"`
Status string `json:"status"`
}
)
@server (
prefix: v1/public/file
group: public/file
middleware: AuthMiddleware,DeviceMiddleware
)
service ppanel {
@doc "Init file upload"
@handler FileUploadInit
post /upload/init (FileUploadInitRequest) returns (FileUploadInitResponse)
@doc "Complete file upload"
@handler FileUploadComplete
post /upload/complete (FileUploadCompleteRequest) returns (FileUploadCompleteResponse)
}
+16 -1
View File
@@ -61,6 +61,21 @@ Trace: # 链路追踪配置 (OpenTelemetry)
Batcher: otlpgrpc # 本地开发留空""; 生产填 otlpgrpc Batcher: otlpgrpc # 本地开发留空""; 生产填 otlpgrpc
Endpoint: "127.0.0.1:4317" # host 网络模式; bridge 模式改为 tempo:4317 Endpoint: "127.0.0.1:4317" # host 网络模式; bridge 模式改为 tempo:4317
S3:
Enable: false
Region: ""
Bucket: ""
Endpoint: ""
AccessKey: ""
SecretKey: ""
SessionToken: ""
Prefix: "app-upload"
PublicBaseURL: ""
UsePathStyle: false
PresignExpireSeconds: 300
MaxUploadSize: 104857600
AllowedContentTypes: "application/zip,application/x-zip-compressed,application/gzip,application/x-gzip,application/octet-stream,text/plain,application/json"
device: device:
enable: true # 开启设备加密通信 enable: true # 开启设备加密通信
security_secret: "" # AES加密密钥,需要和App端一致,key=SHA256(security_secret)[:32] security_secret: "" # AES加密密钥,需要和App端一致,key=SHA256(security_secret)[:32]
@@ -72,4 +87,4 @@ Administrator:
Register: Register:
EnableTrial: true EnableTrial: true
EnableTrialEmailWhitelist: true EnableTrialEmailWhitelist: true
TrialEmailDomainWhitelist: "facebook.com,yahoo.com,qq.com,live.com,outlook.com,msn.com,example.com,go.com,aol.com,free.fr,aliyun.com,163.com,yandex.ru,indiatimes.com,alibaba.com,geocities.com,about.com,naver.com,netscape.com,yahoo.co.jp,earthlink.net,zoho.com,sky.com,mail.ru,angelfire.com,uol.com.br,spb.ru,yandex.com,globo.com,gmail.com,medscape.com,space.com,discovery.com,t-online.de,mac.com,icloud.com,homestead.com,lycos.com,web.id,nus.edu.sg,altavista.com,berlin.de,rambler.ru,pp.ua,comcast.net,sapo.pt,msk.ru,ancestry.com,daum.net,proton.me,law.com,bt.com,techspot.com,icq.com,sina.cn,libero.it,test.com,yourdomain.com,docomo.ne.jp,wp.pl,hotmail.com,orange.fr,onet.pl,me.com,india.com,kansascity.com,wanadoo.fr,fortunecity.com,web.de,terra.com.br,att.net,canada.com,skynet.be,ya.ru,excite.com,detik.com,compuserve.com,ig.com.br,zp.ua,gmx.net,xoom.com,mindspring.com,freeserve.co.uk,interia.pl,excite.co.jp,test.de,shaw.ca,virgilio.it,chez.com,rr.com,freenet.de,ntlworld.com,seznam.cz,arcor.de,tiscali.it,sympatico.ca,sina.com,gazeta.pl,care2.com,yam.com,r7.com,telenet.be,rcn.com,geek.com,sfr.fr,hotbot.com,cox.net,blueyonder.co.uk,tom.com,virginmedia.com,btinternet.com,iinet.net.au,rogers.com,ireland.com,pochta.ru,ozemail.com.au,catholic.org,bluewin.ch,chat.ru,virgin.net,verizon.net,erols.com,lycos.de,lycos.co.uk,protonmail.com,doityourself.com,home.nl,nate.com,casino.com,o2.co.uk,terra.es,mail.com,albawaba.com,126.com,www.com,planet.nl,sanook.com,21cn.com,online.de,name.com,i.ua,centrum.cz,rin.ru,aol.co.uk,voila.fr,walla.co.il,poste.it,netcom.com,parrot.com,charter.net,mydomain.com,mail-tester.com,myway.com,chello.nl,club-internet.fr,sdf.org,tiscali.co.uk,freeuk.com,unican.es,sci.fi,anonymize.com,sify.com,metacrawler.com,go.ro,ivillage.com,telus.net,dailypioneer.com,iespana.es,lycos.es,hey.com,sweb.cz,optusnet.com.au,alice.it,tpg.com.au,hamptonroads.com,saudia.com,lycos.nl,blackplanet.com,frontier.com,looksmart.com,pobox.com,prodigy.net,i.am,freeyellow.com,gmx.com,bigpond.com,crosswinds.net,dejanews.com,wanadoo.es,foxmail.com,eircom.net,islamonline.net,webindia123.com,oath.com,frontiernet.net,hetnet.nl,onmilwaukee.com,ukr.net,bugmenot.com,neuf.fr,kiwibox.com,za.com,iol.it,zonnet.nl,newmail.ru,pacbell.net,cogeco.ca,depechemode.com,concentric.net,aim.com,f5.si,yahoo.jp,terra.com,hot.ee,netzero.net,netins.net,sprynet.com,mailbox.org,mail2web.com,o2.pl,idirect.com,bigfoot.com,netspace.net.au,masrawy.com,supereva.it,yahoo.de,lycos.it,yeah.net,montevideo.com.uy,gmx.de,yahoo.co.uk,yahoofs.com,scubadiving.com,hushmail.com,iprimus.com.au,gportal.hu,swissinfo.org,inbox.com,bolt.com,telstra.com,bellsouth.net,spray.se,c3.hu,attbi.com,talktalk.co.uk,dynu.net,juno.com,yahoo.fr,msn.co.uk,fr.nf,pe.hu,bigpond.net.au,incredimail.com,adelphia.net,elvis.com,interfree.it,starmedia.com,seanet.com,yahoo.com.tw,zip.net,tds.net,she.com,forthnet.gr,land.ru,wow.com,dnsmadeeasy.com,webjump.com,singnet.com.sg,spacewar.com,tin.it,4mg.com,sp.nl,wowway.com,dmv.com,bangkok.com,fastmail.fm,sbcglobal.net,bright.net,usa.com,37.com,aeiou.pt,terra.cl,thirdage.com,btconnect.com,optimum.net,cableone.net,talkcity.com,blogos.com,c2i.net,iwon.com,aver.com,barcelona.com,ddnsfree.com,oi.com.br,lex.bg,roadrunner.com,airmail.net,lawyer.com,yahoo.com.cn,cu.cc,ananzi.co.za,au.ru,pipeline.com,cs.com,3ammagazine.com,gmx.at,qwest.net,btopenworld.com,easypost.com,westnet.com.au,nyc.com,korea.com,front.ru,inbox.lv,yahoo.com.br,ny.com,hispavista.com,abv.bg,mchsi.com,apollo.lv,everyone.net,terra.com.ar,singpost.com,doctor.com,garbage.com,bizhosting.com,go2net.com,clerk.com,games.com,charm.net,onlinehome.de,laposte.net" # 填你的白名单域名,逗号分隔 TrialEmailDomainWhitelist: "facebook.com,yahoo.com,qq.com,live.com,outlook.com,msn.com,example.com,go.com,aol.com,free.fr,aliyun.com,163.com,yandex.ru,indiatimes.com,alibaba.com,geocities.com,about.com,naver.com,netscape.com,yahoo.co.jp,earthlink.net,zoho.com,sky.com,mail.ru,angelfire.com,uol.com.br,spb.ru,yandex.com,globo.com,gmail.com,medscape.com,space.com,discovery.com,t-online.de,mac.com,icloud.com,homestead.com,lycos.com,web.id,nus.edu.sg,altavista.com,berlin.de,rambler.ru,pp.ua,comcast.net,sapo.pt,msk.ru,ancestry.com,daum.net,proton.me,law.com,bt.com,techspot.com,icq.com,sina.cn,libero.it,test.com,yourdomain.com,docomo.ne.jp,wp.pl,hotmail.com,orange.fr,onet.pl,me.com,india.com,kansascity.com,wanadoo.fr,fortunecity.com,web.de,terra.com.br,att.net,canada.com,skynet.be,ya.ru,excite.com,detik.com,compuserve.com,ig.com.br,zp.ua,gmx.net,xoom.com,mindspring.com,freeserve.co.uk,interia.pl,excite.co.jp,test.de,shaw.ca,virgilio.it,chez.com,rr.com,freenet.de,ntlworld.com,seznam.cz,arcor.de,tiscali.it,sympatico.ca,sina.com,gazeta.pl,care2.com,yam.com,r7.com,telenet.be,rcn.com,geek.com,sfr.fr,hotbot.com,cox.net,blueyonder.co.uk,tom.com,virginmedia.com,btinternet.com,iinet.net.au,rogers.com,ireland.com,pochta.ru,ozemail.com.au,catholic.org,bluewin.ch,chat.ru,virgin.net,verizon.net,erols.com,lycos.de,lycos.co.uk,protonmail.com,doityourself.com,home.nl,nate.com,casino.com,o2.co.uk,terra.es,mail.com,albawaba.com,126.com,www.com,planet.nl,sanook.com,21cn.com,online.de,name.com,i.ua,centrum.cz,rin.ru,aol.co.uk,voila.fr,walla.co.il,poste.it,netcom.com,parrot.com,charter.net,mydomain.com,mail-tester.com,myway.com,chello.nl,club-internet.fr,sdf.org,tiscali.co.uk,freeuk.com,unican.es,sci.fi,anonymize.com,sify.com,metacrawler.com,go.ro,ivillage.com,telus.net,dailypioneer.com,iespana.es,lycos.es,hey.com,sweb.cz,optusnet.com.au,alice.it,tpg.com.au,hamptonroads.com,saudia.com,lycos.nl,blackplanet.com,frontier.com,looksmart.com,pobox.com,prodigy.net,i.am,freeyellow.com,gmx.com,bigpond.com,crosswinds.net,dejanews.com,wanadoo.es,foxmail.com,eircom.net,islamonline.net,webindia123.com,oath.com,frontiernet.net,hetnet.nl,onmilwaukee.com,ukr.net,bugmenot.com,neuf.fr,kiwibox.com,za.com,iol.it,zonnet.nl,newmail.ru,pacbell.net,cogeco.ca,depechemode.com,concentric.net,aim.com,f5.si,yahoo.jp,terra.com,hot.ee,netzero.net,netins.net,sprynet.com,mailbox.org,mail2web.com,o2.pl,idirect.com,bigfoot.com,netspace.net.au,masrawy.com,supereva.it,yahoo.de,lycos.it,yeah.net,montevideo.com.uy,gmx.de,yahoo.co.uk,yahoofs.com,scubadiving.com,hushmail.com,iprimus.com.au,gportal.hu,swissinfo.org,inbox.com,bolt.com,telstra.com,bellsouth.net,spray.se,c3.hu,attbi.com,talktalk.co.uk,dynu.net,juno.com,yahoo.fr,msn.co.uk,fr.nf,pe.hu,bigpond.net.au,incredimail.com,adelphia.net,elvis.com,interfree.it,starmedia.com,seanet.com,yahoo.com.tw,zip.net,tds.net,she.com,forthnet.gr,land.ru,wow.com,dnsmadeeasy.com,webjump.com,singnet.com.sg,spacewar.com,tin.it,4mg.com,sp.nl,wowway.com,dmv.com,bangkok.com,fastmail.fm,sbcglobal.net,bright.net,usa.com,37.com,aeiou.pt,terra.cl,thirdage.com,btconnect.com,optimum.net,cableone.net,talkcity.com,blogos.com,c2i.net,iwon.com,aver.com,barcelona.com,ddnsfree.com,oi.com.br,lex.bg,roadrunner.com,airmail.net,lawyer.com,yahoo.com.cn,cu.cc,ananzi.co.za,au.ru,pipeline.com,cs.com,3ammagazine.com,gmx.at,qwest.net,btopenworld.com,easypost.com,westnet.com.au,nyc.com,korea.com,front.ru,inbox.lv,yahoo.com.br,ny.com,hispavista.com,abv.bg,mchsi.com,apollo.lv,everyone.net,terra.com.ar,singpost.com,doctor.com,garbage.com,bizhosting.com,go2net.com,clerk.com,games.com,charm.net,onlinehome.de,laposte.net" # 填你的白名单域名,逗号分隔
+19 -1
View File
@@ -1,6 +1,6 @@
module github.com/perfect-panel/server module github.com/perfect-panel/server
go 1.23.3 go 1.24
require ( require (
github.com/GUAIK-ORG/go-snowflake v0.0.0-20200116064823-220c4260e85f github.com/GUAIK-ORG/go-snowflake v0.0.0-20200116064823-220c4260e85f
@@ -54,6 +54,10 @@ require (
require ( require (
github.com/Masterminds/sprig/v3 v3.3.0 github.com/Masterminds/sprig/v3 v3.3.0
github.com/aws/aws-sdk-go-v2 v1.41.7
github.com/aws/aws-sdk-go-v2/config v1.32.17
github.com/aws/aws-sdk-go-v2/credentials v1.19.16
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0
github.com/fatih/color v1.18.0 github.com/fatih/color v1.18.0
github.com/goccy/go-json v0.10.4 github.com/goccy/go-json v0.10.4
github.com/golang-migrate/migrate/v4 v4.18.2 github.com/golang-migrate/migrate/v4 v4.18.2
@@ -78,6 +82,20 @@ require (
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
github.com/aliyun/credentials-go v1.3.10 // indirect github.com/aliyun/credentials-go v1.3.10 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 // indirect
github.com/aws/smithy-go v1.25.1 // indirect
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect
github.com/bytedance/sonic v1.12.7 // indirect github.com/bytedance/sonic v1.12.7 // indirect
github.com/bytedance/sonic/loader v0.2.3 // indirect github.com/bytedance/sonic/loader v0.2.3 // indirect
+36
View File
@@ -60,6 +60,42 @@ github.com/anaskhan96/go-password-encoder v0.0.0-20201010210601-c765b799fd72 h1:
github.com/anaskhan96/go-password-encoder v0.0.0-20201010210601-c765b799fd72/go.mod h1:PsJICrlruG9QcJDYuZ0dO/2KtMDALzRbony8NkxZ2nE= github.com/anaskhan96/go-password-encoder v0.0.0-20201010210601-c765b799fd72/go.mod h1:PsJICrlruG9QcJDYuZ0dO/2KtMDALzRbony8NkxZ2nE=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD9F4akX5gnN3QZwUB20GGKH/I+Rho=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10/go.mod h1:qqY157uZoqm5OXq/amuaBJyC9hgBCBQnsaWnPe905GY=
github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU=
github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE=
github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU=
github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 h1:ieLCO1JxUWuxTZ1cRd0GAaeX7O6cIxnwk7tc1LsQhC4=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15/go.mod h1:e3IzZvQ3kAWNykvE0Tr0RDZCMFInMvhku3qNpcIQXhM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 h1:03xatSQO4+AM1lTAbnRg5OK528EUg744nW7F73U8DKw=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23/go.mod h1:M8l3mwgx5ToK7wot2sBBce/ojzgnPzZXUV445gTSyE8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 h1:etqBTKY581iwLL/H/S2sVgk3C9lAsTJFeXWFDsDcWOU=
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0/go.mod h1:L2dcoOgS2VSgbPLvpak2NyUPsO1TBN7M45Z4H7DlRc4=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg=
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk=
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio=
github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04= github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04=
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw= github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
+17
View File
@@ -38,12 +38,29 @@ type Config struct {
Log Log `yaml:"Log"` Log Log `yaml:"Log"`
Currency Currency `yaml:"Currency"` Currency Currency `yaml:"Currency"`
Trace trace.Config `yaml:"Trace"` Trace trace.Config `yaml:"Trace"`
S3 S3Config `yaml:"S3"`
Administrator struct { Administrator struct {
Email string `yaml:"Email" default:"admin@ppanel.dev"` Email string `yaml:"Email" default:"admin@ppanel.dev"`
Password string `yaml:"Password" default:"password"` Password string `yaml:"Password" default:"password"`
} `yaml:"Administrator"` } `yaml:"Administrator"`
} }
type S3Config struct {
Enable bool `yaml:"Enable" default:"false"`
Region string `yaml:"Region" default:""`
Bucket string `yaml:"Bucket" default:""`
Endpoint string `yaml:"Endpoint" default:""`
AccessKey string `yaml:"AccessKey" default:""`
SecretKey string `yaml:"SecretKey" default:""`
SessionToken string `yaml:"SessionToken" default:""`
Prefix string `yaml:"Prefix" default:"app-upload"`
PublicBaseURL string `yaml:"PublicBaseURL" default:""`
UsePathStyle bool `yaml:"UsePathStyle" default:"false"`
PresignExpireSeconds int64 `yaml:"PresignExpireSeconds" default:"300"`
MaxUploadSize int64 `yaml:"MaxUploadSize" default:"104857600"`
AllowedContentTypes string `yaml:"AllowedContentTypes" default:"application/zip,application/x-zip-compressed,application/gzip,application/x-gzip,application/octet-stream,text/plain,application/json"`
}
type RedisConfig struct { type RedisConfig struct {
Host string `yaml:"Host" default:"localhost:6379"` Host string `yaml:"Host" default:"localhost:6379"`
Pass string `yaml:"Pass" default:""` Pass string `yaml:"Pass" default:""`
@@ -0,0 +1,26 @@
package file
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/public/file"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Complete file upload
func FileUploadCompleteHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.FileUploadCompleteRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := file.NewFileUploadCompleteLogic(c.Request.Context(), svcCtx)
resp, err := l.FileUploadComplete(&req)
result.HttpResult(c, resp, err)
}
}
@@ -0,0 +1,26 @@
package file
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/public/file"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Init file upload
func FileUploadInitHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.FileUploadInitRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := file.NewFileUploadInitLogic(c.Request.Context(), svcCtx)
resp, err := l.FileUploadInit(&req)
result.HttpResult(c, resp, err)
}
}
+12
View File
@@ -30,6 +30,7 @@ import (
common "github.com/perfect-panel/server/internal/handler/common" common "github.com/perfect-panel/server/internal/handler/common"
publicAnnouncement "github.com/perfect-panel/server/internal/handler/public/announcement" publicAnnouncement "github.com/perfect-panel/server/internal/handler/public/announcement"
publicDocument "github.com/perfect-panel/server/internal/handler/public/document" publicDocument "github.com/perfect-panel/server/internal/handler/public/document"
publicFile "github.com/perfect-panel/server/internal/handler/public/file"
publicIapApple "github.com/perfect-panel/server/internal/handler/public/iap/apple" publicIapApple "github.com/perfect-panel/server/internal/handler/public/iap/apple"
publicOrder "github.com/perfect-panel/server/internal/handler/public/order" publicOrder "github.com/perfect-panel/server/internal/handler/public/order"
publicPayment "github.com/perfect-panel/server/internal/handler/public/payment" publicPayment "github.com/perfect-panel/server/internal/handler/public/payment"
@@ -865,6 +866,17 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
publicDocumentGroupRouter.GET("/list", publicDocument.QueryDocumentListHandler(serverCtx)) publicDocumentGroupRouter.GET("/list", publicDocument.QueryDocumentListHandler(serverCtx))
} }
publicFileGroupRouter := router.Group("/v1/public/file")
publicFileGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
{
// Init file upload
publicFileGroupRouter.POST("/upload/init", publicFile.FileUploadInitHandler(serverCtx))
// Complete file upload
publicFileGroupRouter.POST("/upload/complete", publicFile.FileUploadCompleteHandler(serverCtx))
}
publicOrderGroupRouter := router.Group("/v1/public/order") publicOrderGroupRouter := router.Group("/v1/public/order")
publicOrderGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) publicOrderGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
+148
View File
@@ -0,0 +1,148 @@
package file
import (
"context"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
const (
fileUploadInitStatus = "init"
fileUploadCompleteStatus = "completed"
fileUploadRedisPrefix = "file:upload:"
)
var safeFileNameRegexp = regexp.MustCompile(`[^a-zA-Z0-9._-]+`)
type uploadMeta struct {
FileID string `json:"file_id"`
UserID int64 `json:"user_id"`
BizType string `json:"biz_type"`
FileName string `json:"file_name"`
ContentType string `json:"content_type"`
ObjectKey string `json:"object_key"`
Size int64 `json:"size"`
Sha256 string `json:"sha256"`
Status string `json:"status"`
Etag string `json:"etag,omitempty"`
CreatedAt int64 `json:"created_at"`
CompletedAt int64 `json:"completed_at,omitempty"`
}
func currentUserFromContext(ctx context.Context) (*user.User, error) {
u, ok := ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok || u == nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
return u, nil
}
func allowedContentTypeSet(csv string) map[string]struct{} {
set := make(map[string]struct{})
for _, item := range strings.Split(csv, ",") {
item = strings.TrimSpace(strings.ToLower(item))
if item == "" {
continue
}
set[item] = struct{}{}
}
return set
}
func validateInitRequest(svcCtx *svc.ServiceContext, bizType, fileName, contentType string, size int64) error {
if svcCtx.S3Store == nil || !svcCtx.Config.S3.Enable {
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "S3 upload is not enabled")
}
if strings.TrimSpace(bizType) == "" {
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "biz_type is required")
}
if strings.TrimSpace(fileName) == "" {
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "file_name is required")
}
if size <= 0 {
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "size must be greater than 0")
}
if svcCtx.Config.S3.MaxUploadSize > 0 && size > svcCtx.Config.S3.MaxUploadSize {
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "file size exceeds max limit")
}
allowed := allowedContentTypeSet(svcCtx.Config.S3.AllowedContentTypes)
if len(allowed) > 0 {
if _, ok := allowed[strings.ToLower(strings.TrimSpace(contentType))]; !ok {
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "content_type is not allowed")
}
}
return nil
}
func buildFileID(userID int64, bizType, fileName string) string {
h := sha1.New()
_, _ = h.Write([]byte(fmt.Sprintf("%d:%s:%s:%d", userID, bizType, fileName, time.Now().UnixNano())))
return hex.EncodeToString(h.Sum(nil))[:24]
}
func safeFileName(name string) string {
base := filepath.Base(strings.TrimSpace(name))
if base == "." || base == "/" || base == "" {
return "file.bin"
}
safe := safeFileNameRegexp.ReplaceAllString(base, "_")
if safe == "" {
return "file.bin"
}
return safe
}
func buildObjectKey(prefix string, userID int64, bizType, fileID, fileName string, now time.Time) string {
prefix = strings.Trim(prefix, "/")
if prefix == "" {
prefix = "app-upload"
}
return fmt.Sprintf("%s/%s/%d/%04d/%02d/%s_%s",
prefix,
strings.Trim(safeFileNameRegexp.ReplaceAllString(strings.ToLower(strings.TrimSpace(bizType)), "-"), "-"),
userID,
now.Year(),
int(now.Month()),
fileID,
safeFileName(fileName),
)
}
func uploadRedisKey(fileID string) string {
return fileUploadRedisPrefix + fileID
}
func saveUploadMeta(ctx context.Context, svcCtx *svc.ServiceContext, meta *uploadMeta, ttl time.Duration) error {
data, err := json.Marshal(meta)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "marshal upload meta failed")
}
if err := svcCtx.Redis.Set(ctx, uploadRedisKey(meta.FileID), data, ttl).Err(); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "save upload meta failed")
}
return nil
}
func loadUploadMeta(ctx context.Context, svcCtx *svc.ServiceContext, fileID string) (*uploadMeta, error) {
val, err := svcCtx.Redis.Get(ctx, uploadRedisKey(fileID)).Result()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "upload meta not found")
}
var meta uploadMeta
if err := json.Unmarshal([]byte(val), &meta); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "parse upload meta failed")
}
return &meta, nil
}
@@ -0,0 +1,71 @@
// Code scaffolded by goctl. Safe to edit.
// goctl 1.9.2
package file
import (
"context"
"time"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type FileUploadCompleteLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Complete file upload
func NewFileUploadCompleteLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FileUploadCompleteLogic {
return &FileUploadCompleteLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *FileUploadCompleteLogic) FileUploadComplete(req *types.FileUploadCompleteRequest) (resp *types.FileUploadCompleteResponse, err error) {
u, err := currentUserFromContext(l.ctx)
if err != nil {
return nil, err
}
meta, err := loadUploadMeta(l.ctx, l.svcCtx, req.FileId)
if err != nil {
return nil, err
}
if meta.UserID != u.Id {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
head, err := l.svcCtx.S3Store.HeadObject(l.ctx, meta.ObjectKey)
if err != nil {
l.Errorw("head object failed", logger.Field("error", err.Error()), logger.Field("file_id", meta.FileID))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "uploaded file not found")
}
if meta.Size > 0 && head.ContentLength != meta.Size {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "uploaded file size mismatch")
}
meta.Status = fileUploadCompleteStatus
meta.Etag = head.ETag
meta.ContentType = head.ContentType
meta.CompletedAt = time.Now().Unix()
if err := saveUploadMeta(l.ctx, l.svcCtx, meta, 7*24*time.Hour); err != nil {
return nil, err
}
return &types.FileUploadCompleteResponse{
FileId: meta.FileID,
ObjectKey: meta.ObjectKey,
Size: head.ContentLength,
ContentType: head.ContentType,
Etag: head.ETag,
Status: meta.Status,
}, nil
}
@@ -0,0 +1,80 @@
// Code scaffolded by goctl. Safe to edit.
// goctl 1.9.2
package file
import (
"context"
"time"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
)
type FileUploadInitLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Init file upload
func NewFileUploadInitLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FileUploadInitLogic {
return &FileUploadInitLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *FileUploadInitLogic) FileUploadInit(req *types.FileUploadInitRequest) (resp *types.FileUploadInitResponse, err error) {
u, err := currentUserFromContext(l.ctx)
if err != nil {
return nil, err
}
if err := validateInitRequest(l.svcCtx, req.BizType, req.FileName, req.ContentType, req.Size); err != nil {
return nil, err
}
now := time.Now()
fileID := buildFileID(u.Id, req.BizType, req.FileName)
objectKey := buildObjectKey(l.svcCtx.Config.S3.Prefix, u.Id, req.BizType, fileID, req.FileName, now)
expireSeconds := l.svcCtx.Config.S3.PresignExpireSeconds
if expireSeconds <= 0 {
expireSeconds = 300
}
presigned, err := l.svcCtx.S3Store.PresignPutObject(l.ctx, objectKey, req.ContentType, time.Duration(expireSeconds)*time.Second)
if err != nil {
l.Errorw("presign put object failed", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
return nil, err
}
meta := &uploadMeta{
FileID: fileID,
UserID: u.Id,
BizType: req.BizType,
FileName: req.FileName,
ContentType: req.ContentType,
ObjectKey: objectKey,
Size: req.Size,
Sha256: req.Sha256,
Status: fileUploadInitStatus,
CreatedAt: now.Unix(),
}
if err := saveUploadMeta(l.ctx, l.svcCtx, meta, 24*time.Hour); err != nil {
return nil, err
}
return &types.FileUploadInitResponse{
FileId: fileID,
ObjectKey: objectKey,
UploadURL: presigned.URL,
Method: presigned.Method,
Headers: map[string]string{
"Content-Type": req.ContentType,
},
ExpiredAt: presigned.ExpiresAt,
}, nil
}
+9
View File
@@ -8,6 +8,7 @@ import (
"github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/internal/model/node"
"github.com/perfect-panel/server/internal/model/redemption" "github.com/perfect-panel/server/internal/model/redemption"
"github.com/perfect-panel/server/pkg/device" "github.com/perfect-panel/server/pkg/device"
"github.com/perfect-panel/server/pkg/storage"
"github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/config"
"github.com/perfect-panel/server/internal/model/ads" "github.com/perfect-panel/server/internal/model/ads"
@@ -44,6 +45,7 @@ type ServiceContext struct {
ExchangeRate float64 ExchangeRate float64
GeoIP *IPLocation GeoIP *IPLocation
SignatureValidator *signature.Validator SignatureValidator *signature.Validator
S3Store *storage.S3Store
//NodeCache *cache.NodeCacheClient //NodeCache *cache.NodeCacheClient
AuthModel auth.Model AuthModel auth.Model
@@ -143,6 +145,13 @@ func NewServiceContext(c config.Config) *ServiceContext {
} }
srv.IAPAppleTransactionModel = iapapple.NewModel(db, rds) srv.IAPAppleTransactionModel = iapapple.NewModel(db, rds)
srv.DeviceManager = NewDeviceManager(srv) srv.DeviceManager = NewDeviceManager(srv)
if c.S3.Enable {
s3Store, err := storage.NewS3Store(context.Background(), c.S3)
if err != nil {
panic(err.Error())
}
srv.S3Store = s3Store
}
return srv return srv
} }
-69
View File
@@ -1,69 +0,0 @@
package types
// compat_types.go — 手动补充的类型,已同步到 .api 定义
// 下次用 goctl 重新生成 types.go 后,以下类型会自动包含在 types.go 中,届时可以删除本文件中对应的定义:
// - ContactRequest (已加入 common.api)
// - GetDownloadLinkRequest / GetDownloadLinkResponse (已加入 common.api)
// - EmailLoginRequest (已更新 auth.api, 加入 form tag)
// - ReportLogMessageRequest / ReportLogMessageResponse (已加入 common.api, 路由 POST /v1/common/log/report)
// - LegacyCheckVerificationCodeRequest / LegacyCheckVerificationCodeResponse (已加入 common.api, 路由 POST /v1/common/check_code)
type ContactRequest struct {
Name string `json:"name" validate:"required,max=100"`
Email string `json:"email" validate:"required,email"`
OtherContact string `json:"other_contact" validate:"max=200"`
Notes string `json:"notes" validate:"max=2000"`
}
type GetDownloadLinkRequest struct {
InviteCode string `form:"invite_code,optional"`
Platform string `form:"platform" validate:"required,oneof=windows mac ios android"`
}
type GetDownloadLinkResponse struct {
Url string `json:"url"`
}
type ReportLogMessageRequest struct {
Platform string `json:"platform" validate:"required,max=32"`
AppVersion string `json:"app_version" validate:"required,max=64"`
OsName string `json:"os_name" validate:"max=64"`
OsVersion string `json:"os_version" validate:"max=64"`
DeviceId string `json:"device_id" validate:"required,max=255"`
UserId int64 `json:"user_id"`
SessionId string `json:"session_id" validate:"max=255"`
Level uint8 `json:"level"`
ErrorCode string `json:"error_code" validate:"max=128"`
Message string `json:"message" validate:"required,max=65535"`
Stack string `json:"stack" validate:"max=1048576"`
Context interface{} `json:"context"`
OccurredAt int64 `json:"occurred_at"`
}
type ReportLogMessageResponse struct {
Id int64 `json:"id"`
}
type LegacyCheckVerificationCodeRequest struct {
Method string `json:"method" form:"method" validate:"omitempty,oneof=email mobile"`
Account string `json:"account" form:"account"`
Email string `json:"email" form:"email"`
Code string `json:"code" form:"code" validate:"required"`
Type uint8 `json:"type" form:"type" validate:"required"`
}
type LegacyCheckVerificationCodeResponse struct {
Status bool `json:"status"`
Exist bool `json:"exist"`
}
type EmailLoginRequest struct {
Identifier string `json:"identifier" form:"identifier"`
Email string `json:"email" form:"email" validate:"required,email"`
Code string `json:"code" form:"code" validate:"required"`
Invite string `json:"invite" form:"invite"`
IP string `header:"X-Original-Forwarded-For"`
UserAgent string `header:"User-Agent"`
LoginType string `header:"Login-Type"`
CfToken string `json:"cf_token,optional" form:"cf_token"`
}
+125 -35
View File
@@ -1,8 +1,12 @@
// Code generated by goctl. DO NOT EDIT. // Code generated by goctl. DO NOT EDIT.
// goctl 1.7.2 // goctl 1.9.2
package types package types
type ActivateOrderRequest struct {
OrderNo string `json:"order_no" validate:"required"`
}
type Ads struct { type Ads struct {
Id int `json:"id"` Id int `json:"id"`
Title string `json:"title"` Title string `json:"title"`
@@ -295,6 +299,13 @@ type ConnectionRecords struct {
LongestSingleConnection int64 `json:"longest_single_connection"` LongestSingleConnection int64 `json:"longest_single_connection"`
} }
type ContactRequest struct {
Name string `json:"name" validate:"required,max=100"`
Email string `json:"email" validate:"required,email"`
OtherContact string `json:"other_contact" validate:"max=200"`
Notes string `json:"notes" validate:"max=2000"`
}
type Coupon struct { type Coupon struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -549,6 +560,13 @@ type CurrencyConfig struct {
CurrencySymbol string `json:"currency_symbol"` CurrencySymbol string `json:"currency_symbol"`
} }
type DailyTrafficStats struct {
Date string `json:"date"`
Upload int64 `json:"upload"`
Download int64 `json:"download"`
Total int64 `json:"total"`
}
type DeleteAccountRequest struct { type DeleteAccountRequest struct {
Email string `json:"email" validate:"required,email"` Email string `json:"email" validate:"required,email"`
Code string `json:"code" validate:"required"` Code string `json:"code" validate:"required"`
@@ -561,13 +579,6 @@ type DeleteAccountResponse struct {
Code int64 `json:"code"` Code int64 `json:"code"`
} }
type DailyTrafficStats struct {
Date string `json:"date"`
Upload int64 `json:"upload"`
Download int64 `json:"download"`
Total int64 `json:"total"`
}
type DeleteAdsRequest struct { type DeleteAdsRequest struct {
Id int64 `json:"id"` Id int64 `json:"id"`
} }
@@ -689,8 +700,15 @@ type EmailAuthticateConfig struct {
DomainSuffixList string `json:"domain_suffix_list"` DomainSuffixList string `json:"domain_suffix_list"`
} }
type ExportGroupResultRequest struct { type EmailLoginRequest struct {
HistoryId *int64 `form:"history_id,omitempty"` Identifier string `json:"identifier" form:"identifier"`
Email string `json:"email" form:"email" validate:"required,email"`
Code string `json:"code" form:"code" validate:"required"`
Invite string `json:"invite,optional" form:"invite"`
IP string `header:"X-Original-Forwarded-For"`
UserAgent string `header:"User-Agent"`
LoginType string `header:"Login-Type"`
CfToken string `json:"cf_token,optional" form:"cf_token"`
} }
type ErrorLogMessage struct { type ErrorLogMessage struct {
@@ -708,6 +726,10 @@ type ErrorLogMessage struct {
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
} }
type ExportGroupResultRequest struct {
HistoryId *int64 `form:"history_id,omitempty"`
}
type FamilyDetail struct { type FamilyDetail struct {
Summary FamilySummary `json:"summary"` Summary FamilySummary `json:"summary"`
Members []FamilyMemberItem `json:"members"` Members []FamilyMemberItem `json:"members"`
@@ -740,6 +762,36 @@ type FamilySummary struct {
UpdatedAt int64 `json:"updated_at"` UpdatedAt int64 `json:"updated_at"`
} }
type FileUploadCompleteRequest struct {
FileId string `json:"file_id" validate:"required"`
}
type FileUploadCompleteResponse struct {
FileId string `json:"file_id"`
ObjectKey string `json:"object_key"`
Size int64 `json:"size"`
ContentType string `json:"content_type"`
Etag string `json:"etag"`
Status string `json:"status"`
}
type FileUploadInitRequest struct {
BizType string `json:"biz_type" validate:"required"`
FileName string `json:"file_name" validate:"required"`
ContentType string `json:"content_type" validate:"required"`
Size int64 `json:"size" validate:"required"`
Sha256 string `json:"sha256"`
}
type FileUploadInitResponse struct {
FileId string `json:"file_id"`
ObjectKey string `json:"object_key"`
UploadURL string `json:"upload_url"`
Method string `json:"method"`
Headers map[string]string `json:"headers"`
ExpiredAt int64 `json:"expired_at"`
}
type FilterBalanceLogRequest struct { type FilterBalanceLogRequest struct {
FilterLogParams FilterLogParams
UserId int64 `form:"user_id,optional"` UserId int64 `form:"user_id,optional"`
@@ -1048,6 +1100,15 @@ type GetDocumentListResponse struct {
List []Document `json:"list"` List []Document `json:"list"`
} }
type GetDownloadLinkRequest struct {
InviteCode string `form:"invite_code,optional"`
Platform string `form:"platform" validate:"required,oneof=windows mac ios android"`
}
type GetDownloadLinkResponse struct {
Url string `json:"url"`
}
type GetErrorLogMessageDetailResponse struct { type GetErrorLogMessageDetailResponse struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Platform string `json:"platform"` Platform string `json:"platform"`
@@ -1118,18 +1179,6 @@ type GetGlobalConfigResponse struct {
WebAd bool `json:"web_ad"` WebAd bool `json:"web_ad"`
} }
type GetInviteSalesRequest struct {
Page int `form:"page"`
Size int `form:"size"`
StartTime int64 `form:"start_time"`
EndTime int64 `form:"end_time"`
}
type GetInviteSalesResponse struct {
Total int64 `json:"total"`
List []InvitedUserSale `json:"list"`
}
type GetGroupConfigRequest struct { type GetGroupConfigRequest struct {
Keys []string `form:"keys,omitempty"` Keys []string `form:"keys,omitempty"`
} }
@@ -1161,6 +1210,18 @@ type GetGroupHistoryResponse struct {
List []GroupHistory `json:"list"` List []GroupHistory `json:"list"`
} }
type GetInviteSalesRequest struct {
Page int `form:"page"`
Size int `form:"size"`
StartTime int64 `form:"start_time"`
EndTime int64 `form:"end_time"`
}
type GetInviteSalesResponse struct {
Total int64 `json:"total"`
List []InvitedUserSale `json:"list"`
}
type GetLoginLogRequest struct { type GetLoginLogRequest struct {
Page int `form:"page"` Page int `form:"page"`
Size int `form:"size"` Size int `form:"size"`
@@ -1419,7 +1480,7 @@ type GetUserListRequest struct {
FamilyStatus string `form:"family_status,omitempty"` FamilyStatus string `form:"family_status,omitempty"`
FamilyOwnerUserId *int64 `form:"family_owner_user_id,omitempty"` FamilyOwnerUserId *int64 `form:"family_owner_user_id,omitempty"`
FamilyId *int64 `form:"family_id,omitempty"` FamilyId *int64 `form:"family_id,omitempty"`
SortOrder string `form:"sort_order,omitempty"` // asc or desc, default desc SortOrder string `form:"sort_order,omitempty"`
} }
type GetUserListResponse struct { type GetUserListResponse struct {
@@ -1601,6 +1662,19 @@ type KickOfflineRequest struct {
Id int64 `json:"id"` Id int64 `json:"id"`
} }
type LegacyCheckVerificationCodeRequest struct {
Method string `json:"method" form:"method" validate:"omitempty,oneof=email mobile"`
Account string `json:"account" form:"account"`
Email string `json:"email" form:"email"`
Code string `json:"code" form:"code" validate:"required"`
Type uint8 `json:"type" form:"type" validate:"required"`
}
type LegacyCheckVerificationCodeResponse struct {
Status bool `json:"status"`
Exist bool `json:"exist"`
}
type LogResponse struct { type LogResponse struct {
List interface{} `json:"list"` List interface{} `json:"list"`
} }
@@ -1765,8 +1839,8 @@ type Order struct {
TradeNo string `json:"trade_no"` TradeNo string `json:"trade_no"`
Status uint8 `json:"status"` Status uint8 `json:"status"`
SubscribeId int64 `json:"subscribe_id"` SubscribeId int64 `json:"subscribe_id"`
CreatedAt int64 `json:"created_at"` // Unix milliseconds CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"` // Unix milliseconds UpdatedAt int64 `json:"updated_at"`
} }
type OrderDetail struct { type OrderDetail struct {
@@ -1789,8 +1863,8 @@ type OrderDetail struct {
Status uint8 `json:"status"` Status uint8 `json:"status"`
SubscribeId int64 `json:"subscribe_id"` SubscribeId int64 `json:"subscribe_id"`
Subscribe Subscribe `json:"subscribe"` Subscribe Subscribe `json:"subscribe"`
CreatedAt int64 `json:"created_at"` // Unix milliseconds CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"` // Unix milliseconds UpdatedAt int64 `json:"updated_at"`
} }
type OrdersStatistics struct { type OrdersStatistics struct {
@@ -2246,6 +2320,10 @@ type RecoverOrderResponse struct {
Message string `json:"message"` Message string `json:"message"`
} }
type RecoverySendCodeRequest struct {
Email string `json:"email" validate:"required,email"`
}
type RedeemCodeRequest struct { type RedeemCodeRequest struct {
Code string `json:"code" validate:"required"` Code string `json:"code" validate:"required"`
} }
@@ -2319,6 +2397,26 @@ type RenewalOrderResponse struct {
AppAccountToken string `json:"app_account_token"` AppAccountToken string `json:"app_account_token"`
} }
type ReportLogMessageRequest struct {
Platform string `json:"platform" validate:"required,max=32"`
AppVersion string `json:"app_version" validate:"required,max=64"`
OsName string `json:"os_name" validate:"max=64"`
OsVersion string `json:"os_version" validate:"max=64"`
DeviceId string `json:"device_id" validate:"required,max=255"`
UserId int64 `json:"user_id"`
SessionId string `json:"session_id" validate:"max=255"`
Level uint8 `json:"level"`
ErrorCode string `json:"error_code" validate:"max=128"`
Message string `json:"message" validate:"required,max=65535"`
Stack string `json:"stack" validate:"max=1048576"`
Context interface{} `json:"context"`
OccurredAt int64 `json:"occurred_at"`
}
type ReportLogMessageResponse struct {
Id int64 `json:"id"`
}
type ResetAllSubscribeTokenResponse struct { type ResetAllSubscribeTokenResponse struct {
Success bool `json:"success"` Success bool `json:"success"`
} }
@@ -2370,10 +2468,6 @@ type ResetTrafficOrderResponse struct {
OrderNo string `json:"order_no"` OrderNo string `json:"order_no"`
} }
type RecoverySendCodeRequest struct {
Email string `json:"email" validate:"required,email"`
}
type ResetUserSubscribeTokenRequest struct { type ResetUserSubscribeTokenRequest struct {
UserSubscribeId int64 `json:"user_subscribe_id"` UserSubscribeId int64 `json:"user_subscribe_id"`
} }
@@ -3000,10 +3094,6 @@ type UpdateOrderStatusRequest struct {
TradeNo string `json:"trade_no,omitempty"` TradeNo string `json:"trade_no,omitempty"`
} }
type ActivateOrderRequest struct {
OrderNo string `json:"order_no" validate:"required"`
}
type UpdatePaymentMethodRequest struct { type UpdatePaymentMethodRequest struct {
Id int64 `json:"id" validate:"required"` Id int64 `json:"id" validate:"required"`
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
+128
View File
@@ -0,0 +1,128 @@
package storage
import (
"context"
"fmt"
"net/url"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/perfect-panel/server/internal/config"
)
type PresignUploadResult struct {
URL string
Method string
ExpiresAt int64
}
type ObjectMeta struct {
ETag string
ContentType string
ContentLength int64
}
type S3Store struct {
cfg config.S3Config
client *s3.Client
presign *s3.PresignClient
}
func NewS3Store(ctx context.Context, cfg config.S3Config) (*S3Store, error) {
loaders := make([]func(*awsconfig.LoadOptions) error, 0, 3)
if cfg.Region != "" {
loaders = append(loaders, awsconfig.WithRegion(cfg.Region))
}
if cfg.AccessKey != "" || cfg.SecretKey != "" || cfg.SessionToken != "" {
loaders = append(loaders, awsconfig.WithCredentialsProvider(
credentials.NewStaticCredentialsProvider(cfg.AccessKey, cfg.SecretKey, cfg.SessionToken),
))
}
awsCfg, err := awsconfig.LoadDefaultConfig(ctx, loaders...)
if err != nil {
return nil, err
}
client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
o.UsePathStyle = cfg.UsePathStyle
if cfg.Endpoint != "" {
o.BaseEndpoint = aws.String(cfg.Endpoint)
}
})
return &S3Store{
cfg: cfg,
client: client,
presign: s3.NewPresignClient(client),
}, nil
}
func (s *S3Store) PresignPutObject(ctx context.Context, objectKey string, contentType string, expires time.Duration) (*PresignUploadResult, error) {
req, err := s.presign.PresignPutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(s.cfg.Bucket),
Key: aws.String(objectKey),
ContentType: aws.String(contentType),
}, s3.WithPresignExpires(expires))
if err != nil {
return nil, err
}
return &PresignUploadResult{
URL: req.URL,
Method: req.Method,
ExpiresAt: time.Now().Add(expires).Unix(),
}, nil
}
func (s *S3Store) HeadObject(ctx context.Context, objectKey string) (*ObjectMeta, error) {
resp, err := s.client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(s.cfg.Bucket),
Key: aws.String(objectKey),
})
if err != nil {
return nil, err
}
return &ObjectMeta{
ETag: strings.Trim(aws.ToString(resp.ETag), "\""),
ContentType: aws.ToString(resp.ContentType),
ContentLength: aws.ToInt64(resp.ContentLength),
}, nil
}
func (s *S3Store) BuildObjectURL(objectKey string) string {
if s.cfg.PublicBaseURL != "" {
return strings.TrimRight(s.cfg.PublicBaseURL, "/") + "/" + strings.TrimLeft(objectKey, "/")
}
if s.cfg.Endpoint != "" {
base := strings.TrimRight(s.cfg.Endpoint, "/")
return fmt.Sprintf("%s/%s/%s", base, url.PathEscape(s.cfg.Bucket), strings.TrimLeft(objectKey, "/"))
}
return fmt.Sprintf("https://%s.s3.%s.amazonaws.com/%s", s.cfg.Bucket, s.cfg.Region, strings.TrimLeft(objectKey, "/"))
}
func (s *S3Store) PutJSONTagging(ctx context.Context, objectKey string, values map[string]string) error {
tagging := make([]types.Tag, 0, len(values))
for k, v := range values {
key := k
value := v
tagging = append(tagging, types.Tag{Key: &key, Value: &value})
}
_, err := s.client.PutObjectTagging(ctx, &s3.PutObjectTaggingInput{
Bucket: aws.String(s.cfg.Bucket),
Key: aws.String(objectKey),
Tagging: &types.Tagging{
TagSet: tagging,
},
})
return err
}
+2
View File
@@ -40,6 +40,8 @@ import (
"apis/public/ticket.api" "apis/public/ticket.api"
"apis/public/payment.api" "apis/public/payment.api"
"apis/public/document.api" "apis/public/document.api"
"apis/public/file.api"
"apis/public/portal.api" "apis/public/portal.api"
"apis/public/iap.api" "apis/public/iap.api"
) )