Rename rest to code.
This commit is contained in:
425
code/alien_controller.go
Normal file
425
code/alien_controller.go
Normal file
@ -0,0 +1,425 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"tank/code/result"
|
||||
"tank/code/tool"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AlienController struct {
|
||||
BaseController
|
||||
uploadTokenDao *UploadTokenDao
|
||||
downloadTokenDao *DownloadTokenDao
|
||||
matterDao *MatterDao
|
||||
matterService *MatterService
|
||||
imageCacheDao *ImageCacheDao
|
||||
imageCacheService *ImageCacheService
|
||||
alienService *AlienService
|
||||
}
|
||||
|
||||
//初始化方法
|
||||
func (this *AlienController) Init() {
|
||||
this.BaseController.Init()
|
||||
|
||||
//手动装填本实例的Bean.
|
||||
b := CONTEXT.GetBean(this.uploadTokenDao)
|
||||
if c, ok := b.(*UploadTokenDao); ok {
|
||||
this.uploadTokenDao = c
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.downloadTokenDao)
|
||||
if c, ok := b.(*DownloadTokenDao); ok {
|
||||
this.downloadTokenDao = c
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.matterDao)
|
||||
if c, ok := b.(*MatterDao); ok {
|
||||
this.matterDao = c
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.matterService)
|
||||
if c, ok := b.(*MatterService); ok {
|
||||
this.matterService = c
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.imageCacheDao)
|
||||
if c, ok := b.(*ImageCacheDao); ok {
|
||||
this.imageCacheDao = c
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.imageCacheService)
|
||||
if c, ok := b.(*ImageCacheService); ok {
|
||||
this.imageCacheService = c
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.alienService)
|
||||
if c, ok := b.(*AlienService); ok {
|
||||
this.alienService = c
|
||||
}
|
||||
}
|
||||
|
||||
//注册自己的路由。
|
||||
func (this *AlienController) RegisterRoutes() map[string]func(writer http.ResponseWriter, request *http.Request) {
|
||||
|
||||
routeMap := make(map[string]func(writer http.ResponseWriter, request *http.Request))
|
||||
|
||||
//每个Controller需要主动注册自己的路由。
|
||||
routeMap["/api/alien/fetch/upload/token"] = this.Wrap(this.FetchUploadToken, USER_ROLE_GUEST)
|
||||
routeMap["/api/alien/fetch/download/token"] = this.Wrap(this.FetchDownloadToken, USER_ROLE_GUEST)
|
||||
routeMap["/api/alien/confirm"] = this.Wrap(this.Confirm, USER_ROLE_GUEST)
|
||||
routeMap["/api/alien/upload"] = this.Wrap(this.Upload, USER_ROLE_GUEST)
|
||||
routeMap["/api/alien/crawl/token"] = this.Wrap(this.CrawlToken, USER_ROLE_GUEST)
|
||||
routeMap["/api/alien/crawl/direct"] = this.Wrap(this.CrawlDirect, USER_ROLE_GUEST)
|
||||
|
||||
return routeMap
|
||||
}
|
||||
|
||||
//处理一些特殊的接口,比如参数包含在路径中,一般情况下,controller不将参数放在url路径中
|
||||
func (this *AlienController) HandleRoutes(writer http.ResponseWriter, request *http.Request) (func(writer http.ResponseWriter, request *http.Request), bool) {
|
||||
|
||||
path := request.URL.Path
|
||||
|
||||
//匹配 /api/alien/preview/{uuid}/{filename} (响应头不包含 content-disposition)
|
||||
reg := regexp.MustCompile(`^/api/alien/preview/([^/]+)/([^/]+)$`)
|
||||
strs := reg.FindStringSubmatch(path)
|
||||
if len(strs) == 3 {
|
||||
var f = func(writer http.ResponseWriter, request *http.Request) {
|
||||
this.Preview(writer, request, strs[1], strs[2])
|
||||
}
|
||||
return f, true
|
||||
}
|
||||
|
||||
//匹配 /api/alien/download/{uuid}/{filename} (响应头包含 content-disposition)
|
||||
reg = regexp.MustCompile(`^/api/alien/download/([^/]+)/([^/]+)$`)
|
||||
strs = reg.FindStringSubmatch(path)
|
||||
if len(strs) == 3 {
|
||||
var f = func(writer http.ResponseWriter, request *http.Request) {
|
||||
this.Download(writer, request, strs[1], strs[2])
|
||||
}
|
||||
return f, true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
//直接从cookie中获取用户信息,或者使用邮箱和密码获取用户
|
||||
func (this *AlienController) CheckRequestUser(writer http.ResponseWriter, request *http.Request) *User {
|
||||
|
||||
//根据用户登录信息取
|
||||
user := this.findUser(writer, request)
|
||||
if user != nil {
|
||||
return user;
|
||||
}
|
||||
|
||||
email := request.FormValue("email")
|
||||
if email == "" {
|
||||
panic("邮箱必填啦")
|
||||
}
|
||||
|
||||
password := request.FormValue("password")
|
||||
if password == "" {
|
||||
panic("密码必填")
|
||||
}
|
||||
|
||||
//验证用户身份合法性。
|
||||
user = this.userDao.FindByEmail(email)
|
||||
if user == nil {
|
||||
panic(`邮箱或密码错误`)
|
||||
} else {
|
||||
if !tool.MatchBcrypt(password, user.Password) {
|
||||
panic(`邮箱或密码错误`)
|
||||
}
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
//系统中的用户x要获取一个UploadToken,用于提供给x信任的用户上传文件。
|
||||
func (this *AlienController) FetchUploadToken(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
//文件名。
|
||||
filename := request.FormValue("filename")
|
||||
if filename == "" {
|
||||
panic("文件名必填")
|
||||
} else if m, _ := regexp.MatchString(`[<>|*?/\\]`, filename); m {
|
||||
panic(fmt.Sprintf(`【%s】不符合要求,文件名中不能包含以下特殊符号:< > | * ? / \`, filename))
|
||||
}
|
||||
|
||||
//什么时间后过期,默认24h
|
||||
expireStr := request.FormValue("expire")
|
||||
expire := 24 * 60 * 60
|
||||
if expireStr != "" {
|
||||
var err error
|
||||
expire, err = strconv.Atoi(expireStr)
|
||||
if err != nil {
|
||||
panic(`过期时间不符合规范`)
|
||||
}
|
||||
if expire < 1 {
|
||||
panic(`过期时间不符合规范`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//文件公有或私有
|
||||
privacyStr := request.FormValue("privacy")
|
||||
var privacy bool
|
||||
if privacyStr == "" {
|
||||
panic(`文件公有性必填`)
|
||||
} else {
|
||||
if privacyStr == TRUE {
|
||||
privacy = true
|
||||
} else if privacyStr == "false" {
|
||||
privacy = false
|
||||
} else {
|
||||
panic(`文件公有性不符合规范`)
|
||||
}
|
||||
}
|
||||
|
||||
//文件大小
|
||||
sizeStr := request.FormValue("size")
|
||||
var size int64
|
||||
if sizeStr == "" {
|
||||
panic(`文件大小必填`)
|
||||
} else {
|
||||
|
||||
var err error
|
||||
size, err = strconv.ParseInt(sizeStr, 10, 64)
|
||||
if err != nil {
|
||||
panic(`文件大小不符合规范`)
|
||||
}
|
||||
if size < 1 {
|
||||
panic(`文件大小不符合规范`)
|
||||
}
|
||||
}
|
||||
|
||||
//文件夹路径,以 / 开头。
|
||||
dir := request.FormValue("dir")
|
||||
|
||||
user := this.CheckRequestUser(writer, request)
|
||||
dirMatter := this.matterService.CreateDirectories(user, dir)
|
||||
|
||||
mm, _ := time.ParseDuration(fmt.Sprintf("%ds", expire))
|
||||
uploadToken := &UploadToken{
|
||||
UserUuid: user.Uuid,
|
||||
FolderUuid: dirMatter.Uuid,
|
||||
MatterUuid: "",
|
||||
ExpireTime: time.Now().Add(mm),
|
||||
Filename: filename,
|
||||
Privacy: privacy,
|
||||
Size: size,
|
||||
Ip: tool.GetIpAddress(request),
|
||||
}
|
||||
|
||||
uploadToken = this.uploadTokenDao.Create(uploadToken)
|
||||
|
||||
return this.Success(uploadToken)
|
||||
|
||||
}
|
||||
|
||||
//系统中的用户x 拿着某个文件的uuid来确认是否其信任的用户已经上传好了。
|
||||
func (this *AlienController) Confirm(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
matterUuid := request.FormValue("matterUuid")
|
||||
if matterUuid == "" {
|
||||
panic("matterUuid必填")
|
||||
}
|
||||
|
||||
user := this.CheckRequestUser(writer, request)
|
||||
|
||||
matter := this.matterDao.CheckByUuid(matterUuid)
|
||||
if matter.UserUuid != user.Uuid {
|
||||
panic("文件不属于你")
|
||||
}
|
||||
|
||||
return this.Success(matter)
|
||||
}
|
||||
|
||||
//系统中的用户x 信任的用户上传文件。这个接口需要支持跨域。
|
||||
func (this *AlienController) Upload(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
//允许跨域请求。
|
||||
this.allowCORS(writer)
|
||||
if request.Method == "OPTIONS" {
|
||||
return this.Success("OK")
|
||||
}
|
||||
|
||||
uploadTokenUuid := request.FormValue("uploadTokenUuid")
|
||||
if uploadTokenUuid == "" {
|
||||
panic("uploadTokenUuid必填")
|
||||
}
|
||||
|
||||
uploadToken := this.uploadTokenDao.FindByUuid(uploadTokenUuid)
|
||||
if uploadToken == nil {
|
||||
panic("uploadTokenUuid无效")
|
||||
}
|
||||
|
||||
if uploadToken.ExpireTime.Before(time.Now()) {
|
||||
panic("uploadToken已失效")
|
||||
}
|
||||
|
||||
user := this.userDao.CheckByUuid(uploadToken.UserUuid)
|
||||
|
||||
err := request.ParseMultipartForm(32 << 20)
|
||||
this.PanicError(err)
|
||||
|
||||
file, handler, err := request.FormFile("file")
|
||||
this.PanicError(err)
|
||||
defer func() {
|
||||
e := file.Close()
|
||||
this.PanicError(e)
|
||||
}()
|
||||
|
||||
if handler.Filename != uploadToken.Filename {
|
||||
panic("文件名称不正确")
|
||||
}
|
||||
|
||||
if handler.Size != uploadToken.Size {
|
||||
panic("文件大小不正确")
|
||||
}
|
||||
|
||||
dirMatter := this.matterDao.CheckWithRootByUuid(uploadToken.FolderUuid, user)
|
||||
|
||||
matter := this.matterService.AtomicUpload(file, user, dirMatter, uploadToken.Filename, uploadToken.Privacy)
|
||||
|
||||
//更新这个uploadToken的信息.
|
||||
uploadToken.ExpireTime = time.Now()
|
||||
this.uploadTokenDao.Save(uploadToken)
|
||||
|
||||
return this.Success(matter)
|
||||
}
|
||||
|
||||
//给一个指定的url,从该url中去拉取文件回来。此处采用uploadToken的模式。
|
||||
func (this *AlienController) CrawlToken(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
//允许跨域请求。
|
||||
this.allowCORS(writer)
|
||||
if request.Method == "OPTIONS" {
|
||||
return this.Success("OK")
|
||||
}
|
||||
|
||||
uploadTokenUuid := request.FormValue("uploadTokenUuid")
|
||||
url := request.FormValue("url")
|
||||
|
||||
if uploadTokenUuid == "" {
|
||||
panic("uploadTokenUuid必填")
|
||||
}
|
||||
|
||||
uploadToken := this.uploadTokenDao.FindByUuid(uploadTokenUuid)
|
||||
if uploadToken == nil {
|
||||
panic("uploadTokenUuid无效")
|
||||
}
|
||||
|
||||
if uploadToken.ExpireTime.Before(time.Now()) {
|
||||
panic("uploadToken已失效")
|
||||
}
|
||||
|
||||
user := this.userDao.CheckByUuid(uploadToken.UserUuid)
|
||||
|
||||
dirMatter := this.matterDao.CheckWithRootByUuid(uploadToken.FolderUuid, user)
|
||||
|
||||
matter := this.matterService.AtomicCrawl(url, uploadToken.Filename, user, dirMatter, uploadToken.Privacy)
|
||||
|
||||
//更新这个uploadToken的信息.
|
||||
uploadToken.ExpireTime = time.Now()
|
||||
this.uploadTokenDao.Save(uploadToken)
|
||||
|
||||
return this.Success(matter)
|
||||
}
|
||||
|
||||
//通过一个url直接上传,无需借助uploadToken.
|
||||
func (this *AlienController) CrawlDirect(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
//文件名。
|
||||
filename := request.FormValue("filename")
|
||||
//文件公有或私有
|
||||
privacyStr := request.FormValue("privacy")
|
||||
//文件夹路径,以 / 开头。
|
||||
dir := request.FormValue("dir")
|
||||
url := request.FormValue("url")
|
||||
|
||||
if filename == "" {
|
||||
panic("文件名必填")
|
||||
} else if m, _ := regexp.MatchString(`[<>|*?/\\]`, filename); m {
|
||||
panic(fmt.Sprintf(`【%s】不符合要求,文件名中不能包含以下特殊符号:< > | * ? / \`, filename))
|
||||
}
|
||||
|
||||
var privacy bool
|
||||
if privacyStr == "" {
|
||||
panic(`文件公有性必填`)
|
||||
} else {
|
||||
if privacyStr == TRUE {
|
||||
privacy = true
|
||||
} else if privacyStr == FALSE {
|
||||
privacy = false
|
||||
} else {
|
||||
panic(`文件公有性不符合规范`)
|
||||
}
|
||||
}
|
||||
|
||||
user := this.CheckRequestUser(writer, request)
|
||||
dirMatter := this.matterService.CreateDirectories(user, dir)
|
||||
|
||||
matter := this.matterService.AtomicCrawl(url, filename, user, dirMatter, privacy)
|
||||
|
||||
return this.Success(matter)
|
||||
}
|
||||
|
||||
//系统中的用户x要获取一个DownloadToken,用于提供给x信任的用户下载文件。
|
||||
func (this *AlienController) FetchDownloadToken(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
matterUuid := request.FormValue("matterUuid")
|
||||
if matterUuid == "" {
|
||||
panic("matterUuid必填")
|
||||
}
|
||||
|
||||
user := this.CheckRequestUser(writer, request)
|
||||
|
||||
matter := this.matterDao.CheckByUuid(matterUuid)
|
||||
if matter.UserUuid != user.Uuid {
|
||||
panic("文件不属于你")
|
||||
}
|
||||
if matter.Dir {
|
||||
panic("不支持下载文件夹")
|
||||
}
|
||||
|
||||
//什么时间后过期,默认24h
|
||||
expireStr := request.FormValue("expire")
|
||||
expire := 24 * 60 * 60
|
||||
if expireStr != "" {
|
||||
var err error
|
||||
expire, err = strconv.Atoi(expireStr)
|
||||
if err != nil {
|
||||
panic(`过期时间不符合规范`)
|
||||
}
|
||||
if expire < 1 {
|
||||
panic(`过期时间不符合规范`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
mm, _ := time.ParseDuration(fmt.Sprintf("%ds", expire))
|
||||
downloadToken := &DownloadToken{
|
||||
UserUuid: user.Uuid,
|
||||
MatterUuid: matterUuid,
|
||||
ExpireTime: time.Now().Add(mm),
|
||||
Ip: tool.GetIpAddress(request),
|
||||
}
|
||||
|
||||
downloadToken = this.downloadTokenDao.Create(downloadToken)
|
||||
|
||||
return this.Success(downloadToken)
|
||||
|
||||
}
|
||||
|
||||
//预览一个文件。既可以使用登录的方式,也可以使用授权的方式
|
||||
func (this *AlienController) Preview(writer http.ResponseWriter, request *http.Request, uuid string, filename string) {
|
||||
|
||||
this.alienService.PreviewOrDownload(writer, request, uuid, filename, false)
|
||||
}
|
||||
|
||||
//下载一个文件。既可以使用登录的方式,也可以使用授权的方式
|
||||
func (this *AlienController) Download(writer http.ResponseWriter, request *http.Request, uuid string, filename string) {
|
||||
|
||||
this.alienService.PreviewOrDownload(writer, request, uuid, filename, true)
|
||||
}
|
141
code/alien_service.go
Normal file
141
code/alien_service.go
Normal file
@ -0,0 +1,141 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"tank/code/result"
|
||||
"tank/code/tool"
|
||||
"time"
|
||||
)
|
||||
|
||||
//@Service
|
||||
type AlienService struct {
|
||||
Bean
|
||||
matterDao *MatterDao
|
||||
matterService *MatterService
|
||||
userDao *UserDao
|
||||
uploadTokenDao *UploadTokenDao
|
||||
downloadTokenDao *DownloadTokenDao
|
||||
imageCacheDao *ImageCacheDao
|
||||
imageCacheService *ImageCacheService
|
||||
}
|
||||
|
||||
//初始化方法
|
||||
func (this *AlienService) Init() {
|
||||
this.Bean.Init()
|
||||
|
||||
//手动装填本实例的Bean. 这里必须要用中间变量方可。
|
||||
b := CONTEXT.GetBean(this.matterDao)
|
||||
if b, ok := b.(*MatterDao); ok {
|
||||
this.matterDao = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.matterService)
|
||||
if b, ok := b.(*MatterService); ok {
|
||||
this.matterService = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.userDao)
|
||||
if b, ok := b.(*UserDao); ok {
|
||||
this.userDao = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.uploadTokenDao)
|
||||
if c, ok := b.(*UploadTokenDao); ok {
|
||||
this.uploadTokenDao = c
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.downloadTokenDao)
|
||||
if c, ok := b.(*DownloadTokenDao); ok {
|
||||
this.downloadTokenDao = c
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.imageCacheDao)
|
||||
if c, ok := b.(*ImageCacheDao); ok {
|
||||
this.imageCacheDao = c
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.imageCacheService)
|
||||
if c, ok := b.(*ImageCacheService); ok {
|
||||
this.imageCacheService = c
|
||||
}
|
||||
}
|
||||
|
||||
//预览或者下载的统一处理.
|
||||
func (this *AlienService) PreviewOrDownload(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
uuid string,
|
||||
filename string,
|
||||
withContentDisposition bool) {
|
||||
|
||||
matter := this.matterDao.CheckByUuid(uuid)
|
||||
|
||||
//判断是否是文件夹
|
||||
if matter.Dir {
|
||||
panic("不支持下载文件夹")
|
||||
}
|
||||
|
||||
if matter.Name != filename {
|
||||
panic("文件信息错误")
|
||||
}
|
||||
|
||||
//验证用户的权限问题。
|
||||
//文件如果是私有的才需要权限
|
||||
if matter.Privacy {
|
||||
|
||||
//1.如果带有downloadTokenUuid那么就按照token的信息去获取。
|
||||
downloadTokenUuid := request.FormValue("downloadTokenUuid")
|
||||
if downloadTokenUuid != "" {
|
||||
downloadToken := this.downloadTokenDao.CheckByUuid(downloadTokenUuid)
|
||||
if downloadToken.ExpireTime.Before(time.Now()) {
|
||||
panic("downloadToken已失效")
|
||||
}
|
||||
|
||||
if downloadToken.MatterUuid != uuid {
|
||||
panic("token和文件信息不一致")
|
||||
}
|
||||
|
||||
tokenUser := this.userDao.CheckByUuid(downloadToken.UserUuid)
|
||||
if matter.UserUuid != tokenUser.Uuid {
|
||||
panic(result.CODE_WRAPPER_UNAUTHORIZED)
|
||||
}
|
||||
|
||||
//下载之后立即过期掉。如果是分块下载的,必须以最终获取到完整的数据为准。
|
||||
downloadToken.ExpireTime = time.Now()
|
||||
this.downloadTokenDao.Save(downloadToken)
|
||||
|
||||
} else {
|
||||
|
||||
//判断文件的所属人是否正确
|
||||
operator := this.findUser(writer, request)
|
||||
if operator == nil || (operator.Role != USER_ROLE_ADMINISTRATOR && matter.UserUuid != operator.Uuid) {
|
||||
panic(result.CODE_WRAPPER_UNAUTHORIZED)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//对图片处理。
|
||||
needProcess, imageResizeM, imageResizeW, imageResizeH := this.imageCacheService.ResizeParams(request)
|
||||
if needProcess {
|
||||
|
||||
//如果是图片,那么能用缓存就用缓存
|
||||
imageCache := this.imageCacheDao.FindByMatterUuidAndMode(matter.Uuid, fmt.Sprintf("%s_%d_%d", imageResizeM, imageResizeW, imageResizeH))
|
||||
if imageCache == nil {
|
||||
imageCache = this.imageCacheService.cacheImage(writer, request, matter)
|
||||
}
|
||||
|
||||
//直接使用缓存中的信息
|
||||
this.matterService.DownloadFile(writer, request, GetUserCacheRootDir(imageCache.Username)+imageCache.Path, imageCache.Name, withContentDisposition)
|
||||
|
||||
} else {
|
||||
this.matterService.DownloadFile(writer, request, matter.AbsolutePath(), matter.Name, withContentDisposition)
|
||||
}
|
||||
|
||||
//文件下载次数加一,为了加快访问速度,异步进行
|
||||
go tool.SafeMethod(func() {
|
||||
this.matterDao.TimesIncrement(uuid)
|
||||
})
|
||||
|
||||
}
|
126
code/base_controller.go
Normal file
126
code/base_controller.go
Normal file
@ -0,0 +1,126 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/json-iterator/go"
|
||||
"go/types"
|
||||
"net/http"
|
||||
"tank/code/result"
|
||||
)
|
||||
|
||||
type IController interface {
|
||||
IBean
|
||||
//注册自己固定的路由。
|
||||
RegisterRoutes() map[string]func(writer http.ResponseWriter, request *http.Request)
|
||||
//处理一些特殊的路由。
|
||||
HandleRoutes(writer http.ResponseWriter, request *http.Request) (func(writer http.ResponseWriter, request *http.Request), bool)
|
||||
}
|
||||
type BaseController struct {
|
||||
Bean
|
||||
userDao *UserDao
|
||||
sessionDao *SessionDao
|
||||
}
|
||||
|
||||
func (this *BaseController) Init() {
|
||||
|
||||
this.Bean.Init()
|
||||
|
||||
//手动装填本实例的Bean.
|
||||
b := CONTEXT.GetBean(this.userDao)
|
||||
if b, ok := b.(*UserDao); ok {
|
||||
this.userDao = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.sessionDao)
|
||||
if b, ok := b.(*SessionDao); ok {
|
||||
this.sessionDao = b
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//注册自己的路由。
|
||||
func (this *BaseController) RegisterRoutes() map[string]func(writer http.ResponseWriter, request *http.Request) {
|
||||
//每个Controller需要主动注册自己的路由。
|
||||
return make(map[string]func(writer http.ResponseWriter, request *http.Request))
|
||||
}
|
||||
|
||||
//处理一些特殊的接口,比如参数包含在路径中,一般情况下,controller不将参数放在url路径中
|
||||
func (this *BaseController) HandleRoutes(writer http.ResponseWriter, request *http.Request) (func(writer http.ResponseWriter, request *http.Request), bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
//需要进行登录验证的wrap包装
|
||||
func (this *BaseController) Wrap(f func(writer http.ResponseWriter, request *http.Request) *result.WebResult, qualifiedRole string) func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
|
||||
//writer和request赋值给自己。
|
||||
|
||||
var webResult *result.WebResult = nil
|
||||
|
||||
//只有游客接口不需要登录
|
||||
if qualifiedRole != USER_ROLE_GUEST {
|
||||
user := this.checkUser(writer, request)
|
||||
|
||||
if user.Status == USER_STATUS_DISABLED {
|
||||
//判断用户是否被禁用。
|
||||
webResult = result.ConstWebResult(result.CODE_WRAPPER_USER_DISABLED)
|
||||
} else {
|
||||
if qualifiedRole == USER_ROLE_ADMINISTRATOR && user.Role != USER_ROLE_ADMINISTRATOR {
|
||||
webResult = result.ConstWebResult(result.CODE_WRAPPER_UNAUTHORIZED)
|
||||
} else {
|
||||
webResult = f(writer, request)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
webResult = f(writer, request)
|
||||
}
|
||||
|
||||
//输出的是json格式
|
||||
if webResult != nil {
|
||||
//返回的内容申明是json,utf-8
|
||||
writer.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
//用json的方式输出返回值。
|
||||
b, err := jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(webResult)
|
||||
|
||||
this.PanicError(err)
|
||||
|
||||
writer.WriteHeader(result.FetchHttpStatus(webResult.Code))
|
||||
|
||||
_, err = fmt.Fprintf(writer, string(b))
|
||||
this.PanicError(err)
|
||||
} else {
|
||||
//输出的内容是二进制的。
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//返回成功的结果。支持放置三种类型 1.字符串 2. WebResult对象 3.空指针 4.任意类型
|
||||
func (this *BaseController) Success(data interface{}) *result.WebResult {
|
||||
var webResult *result.WebResult = nil
|
||||
if value, ok := data.(string); ok {
|
||||
//返回一句普通的消息
|
||||
webResult = &result.WebResult{Code: result.CODE_WRAPPER_OK.Code, Msg: value}
|
||||
} else if value, ok := data.(*result.WebResult); ok {
|
||||
//返回一个webResult对象
|
||||
webResult = value
|
||||
} else if _, ok := data.(types.Nil); ok {
|
||||
//返回一个空指针
|
||||
webResult = result.ConstWebResult(result.CODE_WRAPPER_OK)
|
||||
} else {
|
||||
//返回的类型不明确。
|
||||
webResult = &result.WebResult{Code: result.CODE_WRAPPER_OK.Code, Data: data}
|
||||
}
|
||||
return webResult
|
||||
}
|
||||
|
||||
//允许跨域请求
|
||||
func (this *BaseController) allowCORS(writer http.ResponseWriter) {
|
||||
writer.Header().Add("Access-Control-Allow-Origin", "*")
|
||||
writer.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
|
||||
writer.Header().Add("Access-Control-Max-Age", "3600")
|
||||
}
|
8
code/base_dao.go
Normal file
8
code/base_dao.go
Normal file
@ -0,0 +1,8 @@
|
||||
package code
|
||||
|
||||
type BaseDao struct {
|
||||
Bean
|
||||
}
|
||||
|
||||
|
||||
|
64
code/base_model.go
Normal file
64
code/base_model.go
Normal file
@ -0,0 +1,64 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"math"
|
||||
"reflect"
|
||||
"tank/code/config"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
TRUE = "true"
|
||||
FALSE = "false"
|
||||
)
|
||||
|
||||
type Time time.Time
|
||||
|
||||
type IBase interface {
|
||||
//返回其对应的数据库表名
|
||||
TableName() string
|
||||
}
|
||||
|
||||
//Mysql 5.5只支持一个CURRENT_TIMESTAMP的默认值,因此时间的默认值都使用蓝眼云盘第一个发布版本时间 2018-01-01 00:00:00
|
||||
type Base struct {
|
||||
Uuid string `json:"uuid" gorm:"type:char(36);primary_key;unique"`
|
||||
Sort int64 `json:"sort" gorm:"type:bigint(20) not null"`
|
||||
UpdateTime time.Time `json:"updateTime" gorm:"type:timestamp not null;default:CURRENT_TIMESTAMP"`
|
||||
CreateTime time.Time `json:"createTime" gorm:"type:timestamp not null;default:'2018-01-01 00:00:00'"`
|
||||
}
|
||||
|
||||
//将 Struct 转换成map[string]interface{}类型
|
||||
func (this *Base) Map() map[string]interface{} {
|
||||
t := reflect.TypeOf(this)
|
||||
v := reflect.ValueOf(this)
|
||||
|
||||
var data = make(map[string]interface{})
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
data[t.Field(i).Name] = v.Field(i).Interface()
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (this *Base) TableName() string {
|
||||
return config.TABLE_PREFIX + "base"
|
||||
}
|
||||
|
||||
//分页类
|
||||
type Pager struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
TotalItems int `json:"totalItems"`
|
||||
TotalPages int `json:"totalPages"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func NewPager(page int, pageSize int, totalItems int, data interface{}) *Pager {
|
||||
|
||||
return &Pager{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalItems: totalItems,
|
||||
TotalPages: int(math.Ceil(float64(totalItems) / float64(pageSize))),
|
||||
Data: data,
|
||||
}
|
||||
}
|
84
code/bean.go
Normal file
84
code/bean.go
Normal file
@ -0,0 +1,84 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"tank/code/config"
|
||||
"tank/code/logger"
|
||||
"tank/code/result"
|
||||
"tank/code/tool"
|
||||
)
|
||||
|
||||
type IBean interface {
|
||||
//初始化方法
|
||||
Init()
|
||||
//系统清理方法
|
||||
Cleanup()
|
||||
//所有配置都加载完成后调用的方法,包括数据库加载完毕
|
||||
ConfigPost()
|
||||
//快速的Panic方法
|
||||
PanicError(err error)
|
||||
}
|
||||
|
||||
type Bean struct {
|
||||
logger *logger.Logger
|
||||
}
|
||||
|
||||
func (this *Bean) Init() {
|
||||
this.logger = logger.LOGGER
|
||||
}
|
||||
|
||||
func (this *Bean) ConfigPost() {
|
||||
|
||||
}
|
||||
|
||||
//系统大清理,一般时产品即将上线时,清除脏数据,只执行一次。
|
||||
func (this *Bean) Cleanup() {
|
||||
|
||||
}
|
||||
|
||||
//处理错误的统一方法 可以省去if err!=nil 这段代码
|
||||
func (this *Bean) PanicError(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
//能找到一个user就找到一个
|
||||
func (this *Bean) findUser(writer http.ResponseWriter, request *http.Request) *User {
|
||||
|
||||
//验证用户是否已经登录。
|
||||
//登录身份有效期以数据库中记录的为准
|
||||
sessionId := tool.GetSessionUuidFromRequest(request, config.COOKIE_AUTH_KEY)
|
||||
if sessionId == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
//去缓存中捞取看看
|
||||
cacheItem, err := CONTEXT.SessionCache.Value(sessionId)
|
||||
if err != nil {
|
||||
this.logger.Warn("获取缓存时出错了" + err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
if cacheItem == nil || cacheItem.Data() == nil {
|
||||
this.logger.Warn("cache item中已经不存在了 ")
|
||||
return nil
|
||||
}
|
||||
|
||||
if value, ok := cacheItem.Data().(*User); ok {
|
||||
return value
|
||||
} else {
|
||||
this.logger.Error("cache item中的类型不是*User ")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//获取当前登录的用户,找不到就返回登录错误
|
||||
func (this *Bean) checkUser(writer http.ResponseWriter, request *http.Request) *User {
|
||||
if this.findUser(writer, request) == nil {
|
||||
panic(result.ConstWebResult(result.CODE_WRAPPER_LOGIN))
|
||||
} else {
|
||||
return this.findUser(writer, request)
|
||||
}
|
||||
}
|
400
code/cache/cache.go
vendored
Normal file
400
code/cache/cache.go
vendored
Normal file
@ -0,0 +1,400 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"tank/code/tool"
|
||||
"time"
|
||||
)
|
||||
|
||||
//缓存项
|
||||
//主要借鉴了cache2go https://github.com/muesli/cache2go
|
||||
type CacheItem struct {
|
||||
sync.RWMutex //读写锁
|
||||
//缓存键
|
||||
key interface{}
|
||||
//缓存值
|
||||
data interface{}
|
||||
// 缓存项的生命期
|
||||
duration time.Duration
|
||||
//创建时间
|
||||
createTime time.Time
|
||||
//最后访问时间
|
||||
accessTime time.Time
|
||||
//访问次数
|
||||
count int64
|
||||
// 在删除缓存项之前调用的回调函数
|
||||
deleteCallback func(key interface{})
|
||||
}
|
||||
|
||||
//新建一项缓存
|
||||
func NewCacheItem(key interface{}, duration time.Duration, data interface{}) *CacheItem {
|
||||
t := time.Now()
|
||||
return &CacheItem{
|
||||
key: key,
|
||||
duration: duration,
|
||||
createTime: t,
|
||||
accessTime: t,
|
||||
count: 0,
|
||||
deleteCallback: nil,
|
||||
data: data,
|
||||
}
|
||||
}
|
||||
|
||||
//手动获取一下,保持该项
|
||||
func (item *CacheItem) KeepAlive() {
|
||||
item.Lock()
|
||||
defer item.Unlock()
|
||||
item.accessTime = time.Now()
|
||||
item.count++
|
||||
}
|
||||
|
||||
//返回生命周期
|
||||
func (item *CacheItem) Duration() time.Duration {
|
||||
return item.duration
|
||||
}
|
||||
|
||||
//返回访问时间。可能并发,加锁
|
||||
func (item *CacheItem) AccessTime() time.Time {
|
||||
item.RLock()
|
||||
defer item.RUnlock()
|
||||
return item.accessTime
|
||||
}
|
||||
|
||||
//返回创建时间
|
||||
func (item *CacheItem) CreateTime() time.Time {
|
||||
return item.createTime
|
||||
}
|
||||
|
||||
//返回访问时间。可能并发,加锁
|
||||
func (item *CacheItem) Count() int64 {
|
||||
item.RLock()
|
||||
defer item.RUnlock()
|
||||
return item.count
|
||||
}
|
||||
|
||||
//返回key值
|
||||
func (item *CacheItem) Key() interface{} {
|
||||
return item.key
|
||||
}
|
||||
|
||||
//返回数据
|
||||
func (item *CacheItem) Data() interface{} {
|
||||
return item.data
|
||||
}
|
||||
|
||||
//设置回调函数
|
||||
func (item *CacheItem) SetDeleteCallback(f func(interface{})) {
|
||||
item.Lock()
|
||||
defer item.Unlock()
|
||||
item.deleteCallback = f
|
||||
}
|
||||
|
||||
// 统一管理缓存项的表
|
||||
type CacheTable struct {
|
||||
sync.RWMutex
|
||||
|
||||
//所有缓存项
|
||||
items map[interface{}]*CacheItem
|
||||
// 触发缓存清理的定时器
|
||||
cleanupTimer *time.Timer
|
||||
// 缓存清理周期
|
||||
cleanupInterval time.Duration
|
||||
// 获取一个不存在的缓存项时的回调函数
|
||||
loadData func(key interface{}, args ...interface{}) *CacheItem
|
||||
// 向缓存表增加缓存项时的回调函数
|
||||
addedCallback func(item *CacheItem)
|
||||
// 从缓存表删除一个缓存项时的回调函数
|
||||
deleteCallback func(item *CacheItem)
|
||||
}
|
||||
|
||||
// 返回缓存中存储有多少项
|
||||
func (table *CacheTable) Count() int {
|
||||
table.RLock()
|
||||
defer table.RUnlock()
|
||||
return len(table.items)
|
||||
}
|
||||
|
||||
// 遍历所有项
|
||||
func (table *CacheTable) Foreach(trans func(key interface{}, item *CacheItem)) {
|
||||
table.RLock()
|
||||
defer table.RUnlock()
|
||||
|
||||
for k, v := range table.items {
|
||||
trans(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// SetDataLoader配置一个数据加载的回调,当尝试去请求一个不存在的key的时候调用
|
||||
func (table *CacheTable) SetDataLoader(f func(interface{}, ...interface{}) *CacheItem) {
|
||||
table.Lock()
|
||||
defer table.Unlock()
|
||||
table.loadData = f
|
||||
}
|
||||
|
||||
// 添加时的回调函数
|
||||
func (table *CacheTable) SetAddedCallback(f func(*CacheItem)) {
|
||||
table.Lock()
|
||||
defer table.Unlock()
|
||||
table.addedCallback = f
|
||||
}
|
||||
|
||||
// 删除时的回调函数
|
||||
func (table *CacheTable) SetDeleteCallback(f func(*CacheItem)) {
|
||||
table.Lock()
|
||||
defer table.Unlock()
|
||||
table.deleteCallback = f
|
||||
}
|
||||
|
||||
//终结检查,被自调整的时间触发
|
||||
func (table *CacheTable) checkExpire() {
|
||||
table.Lock()
|
||||
if table.cleanupTimer != nil {
|
||||
table.cleanupTimer.Stop()
|
||||
}
|
||||
if table.cleanupInterval > 0 {
|
||||
table.log("Expiration check triggered after %v for table", table.cleanupInterval)
|
||||
} else {
|
||||
table.log("Expiration check installed for table")
|
||||
}
|
||||
|
||||
// 为了不抢占锁,采用临时的items.
|
||||
items := table.items
|
||||
table.Unlock()
|
||||
|
||||
//为了定时器更准确,我们需要在每一个循环中更新‘now’,不确定是否是有效率的。
|
||||
now := time.Now()
|
||||
smallestDuration := 0 * time.Second
|
||||
for key, item := range items {
|
||||
// 取出我们需要的东西,为了不抢占锁
|
||||
item.RLock()
|
||||
duration := item.duration
|
||||
accessTime := item.accessTime
|
||||
item.RUnlock()
|
||||
|
||||
// 0永久有效
|
||||
if duration == 0 {
|
||||
continue
|
||||
}
|
||||
if now.Sub(accessTime) >= duration {
|
||||
//缓存项已经过期
|
||||
_, e := table.Delete(key)
|
||||
if e != nil {
|
||||
table.log("删除缓存项时出错 %v", e.Error())
|
||||
}
|
||||
} else {
|
||||
//查找最靠近结束生命周期的项目
|
||||
if smallestDuration == 0 || duration-now.Sub(accessTime) < smallestDuration {
|
||||
smallestDuration = duration - now.Sub(accessTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 为下次清理设置间隔,自触发机制
|
||||
table.Lock()
|
||||
table.cleanupInterval = smallestDuration
|
||||
if smallestDuration > 0 {
|
||||
table.cleanupTimer = time.AfterFunc(smallestDuration, func() {
|
||||
go tool.SafeMethod(table.checkExpire)
|
||||
})
|
||||
}
|
||||
table.Unlock()
|
||||
}
|
||||
|
||||
// 添加缓存项
|
||||
func (table *CacheTable) Add(key interface{}, duration time.Duration, data interface{}) *CacheItem {
|
||||
item := NewCacheItem(key, duration, data)
|
||||
|
||||
// 将缓存项放入表中
|
||||
table.Lock()
|
||||
table.log("Adding item with key %v and lifespan of %v to table", key, duration)
|
||||
table.items[key] = item
|
||||
|
||||
// 取出需要的东西,释放锁
|
||||
expDur := table.cleanupInterval
|
||||
addedItem := table.addedCallback
|
||||
table.Unlock()
|
||||
|
||||
// 有回调函数便执行回调
|
||||
if addedItem != nil {
|
||||
addedItem(item)
|
||||
}
|
||||
|
||||
// 如果我们没有设置任何心跳检查定时器或者找一个即将迫近的项目
|
||||
if duration > 0 && (expDur == 0 || duration < expDur) {
|
||||
table.checkExpire()
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
// 从缓存中删除项
|
||||
func (table *CacheTable) Delete(key interface{}) (*CacheItem, error) {
|
||||
table.RLock()
|
||||
r, ok := table.items[key]
|
||||
if !ok {
|
||||
table.RUnlock()
|
||||
return nil, errors.New(fmt.Sprintf("没有找到%s对应的记录", key))
|
||||
}
|
||||
|
||||
// 取出要用到的东西,释放锁
|
||||
deleteCallback := table.deleteCallback
|
||||
table.RUnlock()
|
||||
|
||||
// 调用删除回调函数
|
||||
if deleteCallback != nil {
|
||||
deleteCallback(r)
|
||||
}
|
||||
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
if r.deleteCallback != nil {
|
||||
r.deleteCallback(key)
|
||||
}
|
||||
|
||||
table.Lock()
|
||||
defer table.Unlock()
|
||||
table.log("Deleting item with key %v created on %v and hit %v times from table", key, r.createTime, r.count)
|
||||
delete(table.items, key)
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
//单纯的检查某个键是否存在
|
||||
func (table *CacheTable) Exists(key interface{}) bool {
|
||||
table.RLock()
|
||||
defer table.RUnlock()
|
||||
_, ok := table.items[key]
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
//如果存在,返回false. 如果不存在,就去添加一个键,并且返回true
|
||||
func (table *CacheTable) NotFoundAdd(key interface{}, lifeSpan time.Duration, data interface{}) bool {
|
||||
table.Lock()
|
||||
|
||||
if _, ok := table.items[key]; ok {
|
||||
table.Unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
item := NewCacheItem(key, lifeSpan, data)
|
||||
table.log("Adding item with key %v and lifespan of %v to table", key, lifeSpan)
|
||||
table.items[key] = item
|
||||
|
||||
// 取出需要的内容,释放锁
|
||||
expDur := table.cleanupInterval
|
||||
addedItem := table.addedCallback
|
||||
table.Unlock()
|
||||
|
||||
// 添加回调函数
|
||||
if addedItem != nil {
|
||||
addedItem(item)
|
||||
}
|
||||
|
||||
// 触发过期检查
|
||||
if lifeSpan > 0 && (expDur == 0 || lifeSpan < expDur) {
|
||||
table.checkExpire()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
//从缓存中返回一个被标记的并保持活性的值。你可以传附件的参数到DataLoader回调函数
|
||||
func (table *CacheTable) Value(key interface{}, args ...interface{}) (*CacheItem, error) {
|
||||
table.RLock()
|
||||
r, ok := table.items[key]
|
||||
loadData := table.loadData
|
||||
table.RUnlock()
|
||||
|
||||
if ok {
|
||||
// 更新访问次数和访问时间
|
||||
r.KeepAlive()
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// 有加载数据的方式,就通过loadData函数去加载进来
|
||||
if loadData != nil {
|
||||
item := loadData(key, args...)
|
||||
if item != nil {
|
||||
table.Add(key, item.duration, item.data)
|
||||
return item, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("无法加载到缓存值")
|
||||
}
|
||||
|
||||
//没有找到任何东西,返回nil.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 删除缓存表中的所有项目
|
||||
func (table *CacheTable) Truncate() {
|
||||
table.Lock()
|
||||
defer table.Unlock()
|
||||
|
||||
table.log("Truncate table")
|
||||
|
||||
table.items = make(map[interface{}]*CacheItem)
|
||||
table.cleanupInterval = 0
|
||||
if table.cleanupTimer != nil {
|
||||
table.cleanupTimer.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
//辅助table中排序,统计的
|
||||
type CacheItemPair struct {
|
||||
Key interface{}
|
||||
AccessCount int64
|
||||
}
|
||||
|
||||
type CacheItemPairList []CacheItemPair
|
||||
|
||||
func (p CacheItemPairList) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
func (p CacheItemPairList) Len() int { return len(p) }
|
||||
func (p CacheItemPairList) Less(i, j int) bool { return p[i].AccessCount > p[j].AccessCount }
|
||||
|
||||
// 返回缓存表中被访问最多的项目
|
||||
func (table *CacheTable) MostAccessed(count int64) []*CacheItem {
|
||||
table.RLock()
|
||||
defer table.RUnlock()
|
||||
|
||||
p := make(CacheItemPairList, len(table.items))
|
||||
i := 0
|
||||
for k, v := range table.items {
|
||||
p[i] = CacheItemPair{k, v.count}
|
||||
i++
|
||||
}
|
||||
sort.Sort(p)
|
||||
|
||||
var r []*CacheItem
|
||||
c := int64(0)
|
||||
for _, v := range p {
|
||||
if c >= count {
|
||||
break
|
||||
}
|
||||
|
||||
item, ok := table.items[v.Key]
|
||||
if ok {
|
||||
r = append(r, item)
|
||||
}
|
||||
c++
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
|
||||
// 打印日志
|
||||
func (table *CacheTable) log(format string, v ...interface{}) {
|
||||
//全局日志记录
|
||||
//LOGGER.Info(format, v...)
|
||||
}
|
||||
|
||||
//新建一个缓存Table
|
||||
func NewCacheTable() *CacheTable {
|
||||
return &CacheTable{
|
||||
items: make(map[interface{}]*CacheItem),
|
||||
}
|
||||
}
|
184
code/config/config.go
Normal file
184
code/config/config.go
Normal file
@ -0,0 +1,184 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/json-iterator/go"
|
||||
"io/ioutil"
|
||||
"tank/code/logger"
|
||||
"tank/code/tool"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
//用户身份的cookie字段名
|
||||
COOKIE_AUTH_KEY = "_ak"
|
||||
|
||||
|
||||
//数据库表前缀 tank200表示当前应用版本是tank:2.0.x版,数据库结构发生变化必然是中型升级
|
||||
TABLE_PREFIX = "tank20_"
|
||||
|
||||
//当前版本
|
||||
VERSION = "2.0.0"
|
||||
)
|
||||
|
||||
/*
|
||||
如果你需要在本地127.0.0.1创建默认的数据库和账号,使用以下语句。
|
||||
create database tank;
|
||||
grant all privileges on tank.* to tank identified by 'tank123';
|
||||
flush privileges;
|
||||
*/
|
||||
var CONFIG = &Config{}
|
||||
|
||||
//依赖外部定义的变量。
|
||||
type Config struct {
|
||||
//默认监听端口号
|
||||
ServerPort int
|
||||
//网站是否已经完成安装
|
||||
Installed bool
|
||||
//上传的文件路径,要求不以/结尾。如果没有指定,默认在根目录下的matter文件夹中。eg: /var/www/matter
|
||||
MatterPath string
|
||||
//数据库连接信息。
|
||||
MysqlUrl string
|
||||
//配置文件中的项
|
||||
Item *ConfigItem
|
||||
}
|
||||
|
||||
//和tank.json文件中的键值一一对应。
|
||||
type ConfigItem struct {
|
||||
//默认监听端口号
|
||||
ServerPort int
|
||||
//上传的文件路径,要求不以/结尾。如果没有指定,默认在根目录下的matter文件夹中。eg: /var/www/matter
|
||||
MatterPath string
|
||||
//mysql相关配置。
|
||||
//数据库端口
|
||||
MysqlPort int
|
||||
//数据库Host
|
||||
MysqlHost string
|
||||
//数据库名字
|
||||
MysqlSchema string
|
||||
//用户名
|
||||
MysqlUsername string
|
||||
//密码
|
||||
MysqlPassword string
|
||||
}
|
||||
|
||||
//验证配置文件的正确性。
|
||||
func (this *ConfigItem) validate() bool {
|
||||
|
||||
if this.ServerPort == 0 {
|
||||
logger.LOGGER.Error("ServerPort 未配置")
|
||||
return false
|
||||
} else {
|
||||
//只要配置文件中有配置端口,就使用。
|
||||
CONFIG.ServerPort = this.ServerPort
|
||||
}
|
||||
|
||||
if this.MysqlUsername == "" {
|
||||
logger.LOGGER.Error("MysqlUsername 未配置")
|
||||
return false
|
||||
}
|
||||
|
||||
if this.MysqlPassword == "" {
|
||||
logger.LOGGER.Error("MysqlPassword 未配置")
|
||||
return false
|
||||
}
|
||||
|
||||
if this.MysqlHost == "" {
|
||||
logger.LOGGER.Error("MysqlHost 未配置")
|
||||
return false
|
||||
}
|
||||
|
||||
if this.MysqlPort == 0 {
|
||||
logger.LOGGER.Error("MysqlPort 未配置")
|
||||
return false
|
||||
}
|
||||
|
||||
if this.MysqlSchema == "" {
|
||||
logger.LOGGER.Error("MysqlSchema 未配置")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
//验证配置文件是否完好
|
||||
func (this *Config) Init() {
|
||||
|
||||
//JSON初始化
|
||||
jsoniter.RegisterTypeDecoderFunc("time.Time", func(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
|
||||
//如果使用time.UTC,那么时间会相差8小时
|
||||
t, err := time.ParseInLocation("2006-01-02 15:04:05", iter.ReadString(), time.Local)
|
||||
if err != nil {
|
||||
iter.Error = err
|
||||
return
|
||||
}
|
||||
*((*time.Time)(ptr)) = t
|
||||
})
|
||||
|
||||
jsoniter.RegisterTypeEncoderFunc("time.Time", func(ptr unsafe.Pointer, stream *jsoniter.Stream) {
|
||||
t := *((*time.Time)(ptr))
|
||||
//如果使用time.UTC,那么时间会相差8小时
|
||||
stream.WriteString(t.Local().Format("2006-01-02 15:04:05"))
|
||||
}, nil)
|
||||
|
||||
//默认从6010端口启动
|
||||
this.ServerPort = 6010
|
||||
|
||||
this.ReadFromConfigFile()
|
||||
|
||||
}
|
||||
|
||||
//系统如果安装好了就调用这个方法。
|
||||
func (this *Config) ReadFromConfigFile() {
|
||||
|
||||
//读取配置文件
|
||||
filePath := tool.GetConfPath() + "/tank.json"
|
||||
content, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
logger.LOGGER.Warn("无法找到配置文件:%s 即将进入安装过程!", filePath)
|
||||
this.Installed = false
|
||||
} else {
|
||||
this.Item = &ConfigItem{}
|
||||
logger.LOGGER.Warn("读取配置文件:%s", filePath)
|
||||
err := jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal(content, this.Item)
|
||||
if err != nil {
|
||||
logger.LOGGER.Error("配置文件格式错误! 即将进入安装过程!")
|
||||
this.Installed = false
|
||||
return
|
||||
}
|
||||
|
||||
//验证项是否齐全
|
||||
itemValidate := this.Item.validate()
|
||||
if !itemValidate {
|
||||
logger.LOGGER.Error("配置文件信息不齐全! 即将进入安装过程!")
|
||||
this.Installed = false
|
||||
return
|
||||
}
|
||||
|
||||
//使用配置项中的文件路径
|
||||
if this.Item.MatterPath == "" {
|
||||
this.MatterPath = tool.GetHomePath() + "/matter"
|
||||
} else {
|
||||
this.MatterPath = this.Item.MatterPath
|
||||
}
|
||||
tool.MakeDirAll(CONFIG.MatterPath)
|
||||
|
||||
//使用配置项中的端口
|
||||
if this.Item.ServerPort != 0 {
|
||||
this.ServerPort = this.Item.ServerPort
|
||||
}
|
||||
|
||||
this.MysqlUrl = tool.GetMysqlUrl(this.Item.MysqlPort, this.Item.MysqlHost, this.Item.MysqlSchema, this.Item.MysqlUsername, this.Item.MysqlPassword)
|
||||
this.Installed = true
|
||||
|
||||
logger.LOGGER.Info("使用配置文件:%s", filePath)
|
||||
logger.LOGGER.Info("上传文件存放路径:%s", this.MatterPath)
|
||||
}
|
||||
}
|
||||
|
||||
//系统如果安装好了就调用这个方法。
|
||||
func (this *Config) InstallOk() {
|
||||
|
||||
this.ReadFromConfigFile()
|
||||
}
|
198
code/context.go
Normal file
198
code/context.go
Normal file
@ -0,0 +1,198 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jinzhu/gorm"
|
||||
"reflect"
|
||||
"tank/code/cache"
|
||||
"tank/code/config"
|
||||
"tank/code/logger"
|
||||
)
|
||||
|
||||
//全局唯一的上下文(在main函数中初始化)
|
||||
var CONTEXT = &Context{}
|
||||
|
||||
//上下文,管理数据库连接,管理所有路由请求,管理所有的单例component.
|
||||
type Context struct {
|
||||
//数据库连接
|
||||
DB *gorm.DB
|
||||
//session缓存
|
||||
SessionCache *cache.CacheTable
|
||||
//各类的Bean Map。这里面是包含ControllerMap中所有元素
|
||||
BeanMap map[string]IBean
|
||||
//只包含了Controller的map
|
||||
ControllerMap map[string]IController
|
||||
//处理所有路由请求
|
||||
Router *Router
|
||||
}
|
||||
|
||||
//初始化上下文
|
||||
func (this *Context) Init() {
|
||||
|
||||
//创建一个用于存储session的缓存。
|
||||
this.SessionCache = cache.NewCacheTable()
|
||||
|
||||
//初始化Map
|
||||
this.BeanMap = make(map[string]IBean)
|
||||
this.ControllerMap = make(map[string]IController)
|
||||
|
||||
//注册各类Beans.在这个方法里面顺便把Controller装入ControllerMap中去。
|
||||
this.registerBeans()
|
||||
|
||||
//初始化每个bean.
|
||||
this.initBeans()
|
||||
|
||||
//初始化Router. 这个方法要在Bean注册好了之后才能。
|
||||
this.Router = NewRouter()
|
||||
|
||||
//如果数据库信息配置好了,就直接打开数据库连接 同时执行Bean的ConfigPost方法
|
||||
this.InstallOk()
|
||||
|
||||
}
|
||||
|
||||
func (this *Context) OpenDb() {
|
||||
|
||||
var err error = nil
|
||||
this.DB, err = gorm.Open("mysql", config.CONFIG.MysqlUrl)
|
||||
|
||||
if err != nil {
|
||||
logger.LOGGER.Panic("failed to connect mysql database")
|
||||
}
|
||||
|
||||
//是否打开sql日志(在调试阶段可以打开,以方便查看执行的SQL)
|
||||
this.DB.LogMode(false)
|
||||
}
|
||||
|
||||
func (this *Context) CloseDb() {
|
||||
|
||||
if this.DB != nil {
|
||||
err := this.DB.Close()
|
||||
if err != nil {
|
||||
fmt.Println("关闭数据库连接出错", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//注册一个Bean
|
||||
func (this *Context) registerBean(bean IBean) {
|
||||
|
||||
typeOf := reflect.TypeOf(bean)
|
||||
typeName := typeOf.String()
|
||||
|
||||
if element, ok := bean.(IBean); ok {
|
||||
|
||||
err := fmt.Sprintf("【%s】已经被注册了,跳过。", typeName)
|
||||
if _, ok := this.BeanMap[typeName]; ok {
|
||||
logger.LOGGER.Error(fmt.Sprintf(err))
|
||||
} else {
|
||||
this.BeanMap[typeName] = element
|
||||
|
||||
//看看是不是controller类型,如果是,那么单独放在ControllerMap中。
|
||||
if controller, ok1 := bean.(IController); ok1 {
|
||||
this.ControllerMap[typeName] = controller
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.LOGGER.Panic("注册的【%s】不是Bean类型。", typeName)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//注册各个Beans
|
||||
func (this *Context) registerBeans() {
|
||||
|
||||
//alien
|
||||
this.registerBean(new(AlienController))
|
||||
this.registerBean(new(AlienService))
|
||||
|
||||
//dashboard
|
||||
this.registerBean(new(DashboardController))
|
||||
this.registerBean(new(DashboardDao))
|
||||
this.registerBean(new(DashboardService))
|
||||
|
||||
//downloadToken
|
||||
this.registerBean(new(DownloadTokenDao))
|
||||
|
||||
//imageCache
|
||||
this.registerBean(new(ImageCacheController))
|
||||
this.registerBean(new(ImageCacheDao))
|
||||
this.registerBean(new(ImageCacheService))
|
||||
|
||||
//install
|
||||
this.registerBean(new(InstallController))
|
||||
|
||||
//matter
|
||||
this.registerBean(new(MatterController))
|
||||
this.registerBean(new(MatterDao))
|
||||
this.registerBean(new(MatterService))
|
||||
|
||||
//preference
|
||||
this.registerBean(new(PreferenceController))
|
||||
this.registerBean(new(PreferenceDao))
|
||||
this.registerBean(new(PreferenceService))
|
||||
|
||||
//footprint
|
||||
this.registerBean(new(FootprintController))
|
||||
this.registerBean(new(FootprintDao))
|
||||
this.registerBean(new(FootprintService))
|
||||
|
||||
//session
|
||||
this.registerBean(new(SessionDao))
|
||||
this.registerBean(new(SessionService))
|
||||
|
||||
//uploadToken
|
||||
this.registerBean(new(UploadTokenDao))
|
||||
|
||||
//user
|
||||
this.registerBean(new(UserController))
|
||||
this.registerBean(new(UserDao))
|
||||
this.registerBean(new(UserService))
|
||||
|
||||
//webdav
|
||||
this.registerBean(new(DavController))
|
||||
this.registerBean(new(DavService))
|
||||
|
||||
}
|
||||
|
||||
//从Map中获取某个Bean.
|
||||
func (this *Context) GetBean(bean IBean) IBean {
|
||||
|
||||
typeOf := reflect.TypeOf(bean)
|
||||
typeName := typeOf.String()
|
||||
|
||||
if val, ok := this.BeanMap[typeName]; ok {
|
||||
return val
|
||||
} else {
|
||||
logger.LOGGER.Panic("【%s】没有注册。", typeName)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
//初始化每个Bean
|
||||
func (this *Context) initBeans() {
|
||||
|
||||
for _, bean := range this.BeanMap {
|
||||
bean.Init()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//系统如果安装好了就调用这个方法。
|
||||
func (this *Context) InstallOk() {
|
||||
|
||||
if config.CONFIG.Installed {
|
||||
this.OpenDb()
|
||||
|
||||
for _, bean := range this.BeanMap {
|
||||
bean.ConfigPost()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//销毁的方法
|
||||
func (this *Context) Destroy() {
|
||||
this.CloseDb()
|
||||
}
|
103
code/dashboard_controller.go
Normal file
103
code/dashboard_controller.go
Normal file
@ -0,0 +1,103 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"tank/code/result"
|
||||
)
|
||||
|
||||
type DashboardController struct {
|
||||
BaseController
|
||||
dashboardDao *DashboardDao
|
||||
dashboardService *DashboardService
|
||||
}
|
||||
|
||||
//初始化方法
|
||||
func (this *DashboardController) Init() {
|
||||
this.BaseController.Init()
|
||||
|
||||
//手动装填本实例的Bean. 这里必须要用中间变量方可。
|
||||
b := CONTEXT.GetBean(this.dashboardDao)
|
||||
if b, ok := b.(*DashboardDao); ok {
|
||||
this.dashboardDao = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.dashboardService)
|
||||
if b, ok := b.(*DashboardService); ok {
|
||||
this.dashboardService = b
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//注册自己的路由。
|
||||
func (this *DashboardController) RegisterRoutes() map[string]func(writer http.ResponseWriter, request *http.Request) {
|
||||
|
||||
routeMap := make(map[string]func(writer http.ResponseWriter, request *http.Request))
|
||||
|
||||
//每个Controller需要主动注册自己的路由。
|
||||
routeMap["/api/dashboard/page"] = this.Wrap(this.Page, USER_ROLE_ADMINISTRATOR)
|
||||
routeMap["/api/dashboard/active/ip/top10"] = this.Wrap(this.ActiveIpTop10, USER_ROLE_ADMINISTRATOR)
|
||||
|
||||
return routeMap
|
||||
}
|
||||
|
||||
//过去七天分时调用量
|
||||
func (this *DashboardController) InvokeList(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
return this.Success("")
|
||||
|
||||
}
|
||||
|
||||
//按照分页的方式获取某个图片缓存夹下图片缓存和子图片缓存夹的列表,通常情况下只有一页。
|
||||
func (this *DashboardController) Page(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
//如果是根目录,那么就传入root.
|
||||
pageStr := request.FormValue("page")
|
||||
pageSizeStr := request.FormValue("pageSize")
|
||||
orderCreateTime := request.FormValue("orderCreateTime")
|
||||
orderUpdateTime := request.FormValue("orderUpdateTime")
|
||||
orderSort := request.FormValue("orderSort")
|
||||
orderDt := request.FormValue("orderDt")
|
||||
|
||||
var page int
|
||||
if pageStr != "" {
|
||||
page, _ = strconv.Atoi(pageStr)
|
||||
}
|
||||
|
||||
pageSize := 200
|
||||
if pageSizeStr != "" {
|
||||
tmp, err := strconv.Atoi(pageSizeStr)
|
||||
if err == nil {
|
||||
pageSize = tmp
|
||||
}
|
||||
}
|
||||
|
||||
sortArray := []OrderPair{
|
||||
{
|
||||
key: "create_time",
|
||||
value: orderCreateTime,
|
||||
},
|
||||
{
|
||||
key: "update_time",
|
||||
value: orderUpdateTime,
|
||||
},
|
||||
{
|
||||
key: "sort",
|
||||
value: orderSort,
|
||||
},
|
||||
{
|
||||
key: "dt",
|
||||
value: orderDt,
|
||||
},
|
||||
}
|
||||
|
||||
pager := this.dashboardDao.Page(page, pageSize, "", sortArray)
|
||||
|
||||
return this.Success(pager)
|
||||
}
|
||||
|
||||
|
||||
func (this *DashboardController) ActiveIpTop10(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
list := this.dashboardDao.ActiveIpTop10()
|
||||
return this.Success(list)
|
||||
}
|
119
code/dashboard_dao.go
Normal file
119
code/dashboard_dao.go
Normal file
@ -0,0 +1,119 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/nu7hatch/gouuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DashboardDao struct {
|
||||
BaseDao
|
||||
}
|
||||
|
||||
//创建
|
||||
func (this *DashboardDao) Create(dashboard *Dashboard) *Dashboard {
|
||||
|
||||
timeUUID, _ := uuid.NewV4()
|
||||
dashboard.Uuid = string(timeUUID.String())
|
||||
dashboard.CreateTime = time.Now()
|
||||
dashboard.UpdateTime = time.Now()
|
||||
dashboard.Sort = time.Now().UnixNano() / 1e6
|
||||
db := CONTEXT.DB.Create(dashboard)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return dashboard
|
||||
}
|
||||
|
||||
//修改一条记录
|
||||
func (this *DashboardDao) Save(dashboard *Dashboard) *Dashboard {
|
||||
|
||||
dashboard.UpdateTime = time.Now()
|
||||
db := CONTEXT.DB.Save(dashboard)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return dashboard
|
||||
}
|
||||
|
||||
//删除一条记录
|
||||
func (this *DashboardDao) Delete(dashboard *Dashboard) {
|
||||
|
||||
db := CONTEXT.DB.Delete(&dashboard)
|
||||
this.PanicError(db.Error)
|
||||
}
|
||||
|
||||
//按照dt查询
|
||||
func (this *DashboardDao) FindByDt(dt string) *Dashboard {
|
||||
|
||||
// Read
|
||||
var dashboard Dashboard
|
||||
db := CONTEXT.DB.Where(&Dashboard{Dt: dt}).First(&dashboard)
|
||||
if db.Error != nil {
|
||||
return nil
|
||||
}
|
||||
return &dashboard
|
||||
}
|
||||
|
||||
//获取某个文件夹下所有的文件和子文件
|
||||
func (this *DashboardDao) Page(page int, pageSize int, dt string, sortArray []OrderPair) *Pager {
|
||||
|
||||
var wp = &WherePair{}
|
||||
|
||||
if dt != "" {
|
||||
wp = wp.And(&WherePair{Query: "dt = ?", Args: []interface{}{dt}})
|
||||
}
|
||||
|
||||
var conditionDB *gorm.DB
|
||||
conditionDB = CONTEXT.DB.Model(&Dashboard{}).Where(wp.Query, wp.Args...)
|
||||
|
||||
count := 0
|
||||
db := conditionDB.Count(&count)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
var dashboards []*Dashboard
|
||||
db = conditionDB.Order(this.GetSortString(sortArray)).Offset(page * pageSize).Limit(pageSize).Find(&dashboards)
|
||||
this.PanicError(db.Error)
|
||||
pager := NewPager(page, pageSize, count, dashboards)
|
||||
|
||||
return pager
|
||||
}
|
||||
|
||||
//获取最活跃的前10个ip
|
||||
func (this *DashboardDao) ActiveIpTop10() []*DashboardIpTimes {
|
||||
|
||||
var dashboardIpTimes []*DashboardIpTimes
|
||||
|
||||
sortArray := []OrderPair{
|
||||
{
|
||||
key: "times",
|
||||
value: "DESC",
|
||||
},
|
||||
}
|
||||
rows, err := CONTEXT.DB.Model(&Footprint{}).
|
||||
Select("ip,COUNT(uuid) as times").
|
||||
Group("ip").
|
||||
Order(this.GetSortString(sortArray)).
|
||||
Offset(0).
|
||||
Limit(10).
|
||||
Rows()
|
||||
|
||||
this.PanicError(err)
|
||||
for rows.Next() {
|
||||
var ip string;
|
||||
var times int64 = 0;
|
||||
rows.Scan(&ip, ×)
|
||||
item := &DashboardIpTimes{
|
||||
Ip: ip,
|
||||
Times: times,
|
||||
}
|
||||
dashboardIpTimes = append(dashboardIpTimes, item)
|
||||
}
|
||||
|
||||
return dashboardIpTimes
|
||||
}
|
||||
|
||||
//执行清理操作
|
||||
func (this *DashboardDao) Cleanup() {
|
||||
this.logger.Info("[DashboardDao]执行清理:清除数据库中所有Dashboard记录。")
|
||||
db := CONTEXT.DB.Where("uuid is not null").Delete(Dashboard{})
|
||||
this.PanicError(db.Error)
|
||||
}
|
33
code/dashboard_model.go
Normal file
33
code/dashboard_model.go
Normal file
@ -0,0 +1,33 @@
|
||||
package code
|
||||
|
||||
import "tank/code/config"
|
||||
|
||||
/**
|
||||
* 系统的所有访问记录均记录在此
|
||||
*/
|
||||
type Dashboard struct {
|
||||
Base
|
||||
InvokeNum int64 `json:"invokeNum" gorm:"type:bigint(20) not null"` //当日访问量
|
||||
TotalInvokeNum int64 `json:"totalInvokeNum" gorm:"type:bigint(20) not null;default:0"` //截至目前总访问量
|
||||
Uv int64 `json:"uv" gorm:"type:bigint(20) not null;default:0"` //当日UV
|
||||
TotalUv int64 `json:"totalUv" gorm:"type:bigint(20) not null;default:0"` //截至目前总UV
|
||||
MatterNum int64 `json:"matterNum" gorm:"type:bigint(20) not null;default:0"` //文件数量
|
||||
TotalMatterNum int64 `json:"totalMatterNum" gorm:"type:bigint(20) not null;default:0"` //截至目前文件数量
|
||||
FileSize int64 `json:"fileSize" gorm:"type:bigint(20) not null;default:0"` //当日文件大小
|
||||
TotalFileSize int64 `json:"totalFileSize" gorm:"type:bigint(20) not null;default:0"` //截至目前文件总大小
|
||||
AvgCost int64 `json:"avgCost" gorm:"type:bigint(20) not null;default:0"` //请求平均耗时 ms
|
||||
Dt string `json:"dt" gorm:"type:varchar(45) not null;index:idx_dt"` //日期
|
||||
}
|
||||
|
||||
// set File's table name to be `profiles`
|
||||
func (this *Dashboard) TableName() string {
|
||||
return config.TABLE_PREFIX + "dashboard"
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计IP活跃数的
|
||||
*/
|
||||
type DashboardIpTimes struct {
|
||||
Ip string `json:"ip"`
|
||||
Times int64 `json:"times"`
|
||||
}
|
141
code/dashboard_service.go
Normal file
141
code/dashboard_service.go
Normal file
@ -0,0 +1,141 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"tank/code/tool"
|
||||
"time"
|
||||
)
|
||||
|
||||
//@Service
|
||||
type DashboardService struct {
|
||||
Bean
|
||||
dashboardDao *DashboardDao
|
||||
footprintDao *FootprintDao
|
||||
matterDao *MatterDao
|
||||
imageCacheDao *ImageCacheDao
|
||||
userDao *UserDao
|
||||
//每天凌晨定时整理器
|
||||
maintainTimer *time.Timer
|
||||
}
|
||||
|
||||
//初始化方法
|
||||
func (this *DashboardService) Init() {
|
||||
this.Bean.Init()
|
||||
|
||||
//手动装填本实例的Bean. 这里必须要用中间变量方可。
|
||||
b := CONTEXT.GetBean(this.dashboardDao)
|
||||
if b, ok := b.(*DashboardDao); ok {
|
||||
this.dashboardDao = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.footprintDao)
|
||||
if b, ok := b.(*FootprintDao); ok {
|
||||
this.footprintDao = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.matterDao)
|
||||
if b, ok := b.(*MatterDao); ok {
|
||||
this.matterDao = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.imageCacheDao)
|
||||
if b, ok := b.(*ImageCacheDao); ok {
|
||||
this.imageCacheDao = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.userDao)
|
||||
if b, ok := b.(*UserDao); ok {
|
||||
this.userDao = b
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//系统启动,数据库配置完毕后会调用该方法
|
||||
func (this *DashboardService) ConfigPost() {
|
||||
|
||||
//立即执行数据清洗任务
|
||||
go tool.SafeMethod(this.maintain)
|
||||
}
|
||||
|
||||
//每日清洗离线数据表。
|
||||
func (this *DashboardService) maintain() {
|
||||
|
||||
//准备好下次维护日志的时间。
|
||||
now := time.Now()
|
||||
nextTime := tool.FirstMinuteOfDay(tool.Tomorrow())
|
||||
duration := nextTime.Sub(now)
|
||||
this.logger.Info("每日数据汇总,下次时间:%s ", tool.ConvertTimeToDateTimeString(nextTime))
|
||||
this.maintainTimer = time.AfterFunc(duration, func() {
|
||||
go tool.SafeMethod(this.maintain)
|
||||
})
|
||||
|
||||
//准备日期开始结尾
|
||||
startTime := tool.FirstSecondOfDay(tool.Yesterday())
|
||||
endTime := tool.LastSecondOfDay(tool.Yesterday())
|
||||
dt := tool.ConvertTimeToDateString(startTime)
|
||||
longTimeAgo := time.Now()
|
||||
longTimeAgo = longTimeAgo.AddDate(-20, 0, 0)
|
||||
|
||||
this.logger.Info("统计汇总表 %s -> %s", tool.ConvertTimeToDateTimeString(startTime), tool.ConvertTimeToDateTimeString(endTime))
|
||||
|
||||
//判断昨天的记录是否已经生成,如果生成了就直接删除掉
|
||||
dbDashboard := this.dashboardDao.FindByDt(dt)
|
||||
if dbDashboard != nil {
|
||||
this.logger.Info(" %s 的汇总已经存在了,删除以进行更新", dt)
|
||||
this.dashboardDao.Delete(dbDashboard)
|
||||
}
|
||||
|
||||
invokeNum := this.footprintDao.CountBetweenTime(startTime, endTime)
|
||||
this.logger.Info("调用数:%d", invokeNum)
|
||||
|
||||
totalInvokeNum := this.footprintDao.CountBetweenTime(longTimeAgo, endTime)
|
||||
this.logger.Info("历史总调用数:%d", totalInvokeNum)
|
||||
|
||||
uv := this.footprintDao.UvBetweenTime(startTime, endTime)
|
||||
this.logger.Info("UV:%d", uv)
|
||||
|
||||
totalUv := this.footprintDao.UvBetweenTime(longTimeAgo, endTime)
|
||||
this.logger.Info("历史总UV:%d", totalUv)
|
||||
|
||||
matterNum := this.matterDao.CountBetweenTime(startTime, endTime)
|
||||
this.logger.Info("文件数量数:%d", matterNum)
|
||||
|
||||
totalMatterNum := this.matterDao.CountBetweenTime(longTimeAgo, endTime)
|
||||
this.logger.Info("历史文件总数:%d", totalMatterNum)
|
||||
|
||||
var matterSize int64
|
||||
if matterNum != 0 {
|
||||
matterSize = this.matterDao.SizeBetweenTime(startTime, endTime)
|
||||
}
|
||||
this.logger.Info("文件大小:%d", matterSize)
|
||||
|
||||
var totalMatterSize int64
|
||||
if totalMatterNum != 0 {
|
||||
totalMatterSize = this.matterDao.SizeBetweenTime(longTimeAgo, endTime)
|
||||
}
|
||||
this.logger.Info("历史文件总大小:%d", totalMatterSize)
|
||||
|
||||
cacheSize := this.imageCacheDao.SizeBetweenTime(startTime, endTime)
|
||||
this.logger.Info("缓存大小:%d", cacheSize)
|
||||
|
||||
totalCacheSize := this.imageCacheDao.SizeBetweenTime(longTimeAgo, endTime)
|
||||
this.logger.Info("历史缓存总大小:%d", totalCacheSize)
|
||||
|
||||
avgCost := this.footprintDao.AvgCostBetweenTime(startTime, endTime)
|
||||
this.logger.Info("平均耗时:%d ms", avgCost)
|
||||
|
||||
dashboard := &Dashboard{
|
||||
InvokeNum: invokeNum,
|
||||
TotalInvokeNum: totalInvokeNum,
|
||||
Uv: uv,
|
||||
TotalUv: totalUv,
|
||||
MatterNum: matterNum,
|
||||
TotalMatterNum: totalMatterNum,
|
||||
FileSize: matterSize + cacheSize,
|
||||
TotalFileSize: totalMatterSize + totalCacheSize,
|
||||
AvgCost: avgCost,
|
||||
Dt: dt,
|
||||
}
|
||||
|
||||
this.dashboardDao.Create(dashboard)
|
||||
|
||||
}
|
548
code/dav/prop.go
Normal file
548
code/dav/prop.go
Normal file
@ -0,0 +1,548 @@
|
||||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package dav
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"tank/code/dav/xml"
|
||||
"tank/code/result"
|
||||
)
|
||||
|
||||
// Proppatch describes a property update instruction as defined in RFC 4918.
|
||||
// See http://www.webdav.org/specs/rfc4918.html#METHOD_PROPPATCH
|
||||
type Proppatch struct {
|
||||
// Remove specifies whether this patch removes properties. If it does not
|
||||
// remove them, it sets them.
|
||||
Remove bool
|
||||
// Props contains the properties to be set or removed.
|
||||
Props []Property
|
||||
}
|
||||
|
||||
// Propstat describes a XML propstat element as defined in RFC 4918.
|
||||
// See http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat
|
||||
type Propstat struct {
|
||||
// Props contains the properties for which Status applies.
|
||||
Props []Property
|
||||
|
||||
// Status defines the HTTP status code of the properties in Prop.
|
||||
// Allowed values include, but are not limited to the WebDAV status
|
||||
// code extensions for HTTP/1.1.
|
||||
// http://www.webdav.org/specs/rfc4918.html#status.code.extensions.to.http11
|
||||
Status int
|
||||
|
||||
// XMLError contains the XML representation of the optional error element.
|
||||
// XML content within this field must not rely on any predefined
|
||||
// namespace declarations or prefixes. If empty, the XML error element
|
||||
// is omitted.
|
||||
XMLError string
|
||||
|
||||
// ResponseDescription contains the contents of the optional
|
||||
// responsedescription field. If empty, the XML element is omitted.
|
||||
ResponseDescription string
|
||||
}
|
||||
|
||||
|
||||
// DeadPropsHolder holds the dead properties of a resource.
|
||||
//
|
||||
// Dead properties are those properties that are explicitly defined. In
|
||||
// comparison, live properties, such as DAV:getcontentlength, are implicitly
|
||||
// defined by the underlying resource, and cannot be explicitly overridden or
|
||||
// removed. See the Terminology section of
|
||||
// http://www.webdav.org/specs/rfc4918.html#rfc.section.3
|
||||
//
|
||||
// There is a whitelist of the names of live properties. This package handles
|
||||
// all live properties, and will only pass non-whitelisted names to the Patch
|
||||
// method of DeadPropsHolder implementations.
|
||||
type DeadPropsHolder interface {
|
||||
// DeadProps returns a copy of the dead properties held.
|
||||
DeadProps() (map[xml.Name]Property, error)
|
||||
|
||||
// Patch patches the dead properties held.
|
||||
//
|
||||
// Patching is atomic; either all or no patches succeed. It returns (nil,
|
||||
// non-nil) if an internal server error occurred, otherwise the Propstats
|
||||
// collectively contain one Property for each proposed patch Property. If
|
||||
// all patches succeed, Patch returns a slice of length one and a Propstat
|
||||
// element with a 200 OK HTTP status code. If none succeed, for reasons
|
||||
// other than an internal server error, no Propstat has status 200 OK.
|
||||
//
|
||||
// For more details on when various HTTP status codes apply, see
|
||||
// http://www.webdav.org/specs/rfc4918.html#PROPPATCH-status
|
||||
Patch([]Proppatch) ([]Propstat, error)
|
||||
}
|
||||
|
||||
|
||||
func EscapeXML(s string) string {
|
||||
for i := 0; i < len(s); i++ {
|
||||
// As an optimization, if s contains only ASCII letters, digits or a
|
||||
// few special characters, the escaped value is s itself and we don't
|
||||
// need to allocate a buffer and convert between string and []byte.
|
||||
switch c := s[i]; {
|
||||
case c == ' ' || c == '_' ||
|
||||
('+' <= c && c <= '9') || // Digits as well as + , - . and /
|
||||
('A' <= c && c <= 'Z') ||
|
||||
('a' <= c && c <= 'z'):
|
||||
continue
|
||||
}
|
||||
// Otherwise, go through the full escaping process.
|
||||
var buf bytes.Buffer
|
||||
xml.EscapeText(&buf, []byte(s))
|
||||
return buf.String()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
|
||||
// http://www.webdav.org/specs/rfc4918.html#status.code.extensions.to.http11
|
||||
const (
|
||||
StatusMulti = 207
|
||||
StatusUnprocessableEntity = 422
|
||||
StatusLocked = 423
|
||||
StatusFailedDependency = 424
|
||||
StatusInsufficientStorage = 507
|
||||
)
|
||||
|
||||
func StatusText(code int) string {
|
||||
switch code {
|
||||
case StatusMulti:
|
||||
return "Multi-Status"
|
||||
case StatusUnprocessableEntity:
|
||||
return "Unprocessable Entity"
|
||||
case StatusLocked:
|
||||
return "Locked"
|
||||
case StatusFailedDependency:
|
||||
return "Failed Dependency"
|
||||
case StatusInsufficientStorage:
|
||||
return "Insufficient Storage"
|
||||
}
|
||||
return http.StatusText(code)
|
||||
}
|
||||
|
||||
|
||||
var (
|
||||
errInvalidPropfind = errors.New("webdav: invalid propfind")
|
||||
errInvalidResponse = errors.New("webdav: invalid response")
|
||||
)
|
||||
|
||||
|
||||
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_lockinfo
|
||||
type LockInfo struct {
|
||||
XMLName xml.Name `xml:"lockinfo"`
|
||||
Exclusive *struct{} `xml:"lockscope>exclusive"`
|
||||
Shared *struct{} `xml:"lockscope>shared"`
|
||||
Write *struct{} `xml:"locktype>write"`
|
||||
Owner Owner `xml:"owner"`
|
||||
}
|
||||
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_owner
|
||||
type Owner struct {
|
||||
InnerXML string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
|
||||
//这是一个带字节计数器的Reader,可以知道总共读取了多少个字节。
|
||||
type CountingReader struct {
|
||||
n int
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
func (c *CountingReader) Read(p []byte) (int, error) {
|
||||
n, err := c.reader.Read(p)
|
||||
c.n += n
|
||||
return n, err
|
||||
}
|
||||
|
||||
|
||||
// Next returns the next token, if any, in the XML stream of d.
|
||||
// RFC 4918 requires to ignore comments, processing instructions
|
||||
// and directives.
|
||||
// http://www.webdav.org/specs/rfc4918.html#property_values
|
||||
// http://www.webdav.org/specs/rfc4918.html#xml-extensibility
|
||||
func next(d *xml.Decoder) (xml.Token, error) {
|
||||
for {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
switch t.(type) {
|
||||
case xml.Comment, xml.Directive, xml.ProcInst:
|
||||
continue
|
||||
default:
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_prop (for propfind)
|
||||
type PropfindProps []xml.Name
|
||||
|
||||
// UnmarshalXML appends the property names enclosed within start to pn.
|
||||
//
|
||||
// It returns an error if start does not contain any properties or if
|
||||
// properties contain values. Character data between properties is ignored.
|
||||
func (pn *PropfindProps) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
for {
|
||||
t, err := next(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch t.(type) {
|
||||
case xml.EndElement:
|
||||
if len(*pn) == 0 {
|
||||
return fmt.Errorf("%s must not be empty", start.Name.Local)
|
||||
}
|
||||
return nil
|
||||
case xml.StartElement:
|
||||
name := t.(xml.StartElement).Name
|
||||
t, err = next(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, ok := t.(xml.EndElement); !ok {
|
||||
return fmt.Errorf("unexpected token %T", t)
|
||||
}
|
||||
*pn = append(*pn, xml.Name(name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propfind
|
||||
// <!ELEMENT propfind ( propname | (allprop, include?) | prop ) >
|
||||
type Propfind struct {
|
||||
XMLName xml.Name `xml:"DAV: propfind"`
|
||||
Allprop *struct{} `xml:"DAV: allprop"`
|
||||
Propname *struct{} `xml:"DAV: propname"`
|
||||
Prop PropfindProps `xml:"DAV: prop"`
|
||||
Include PropfindProps `xml:"DAV: include"`
|
||||
}
|
||||
|
||||
//从request中读出需要的属性。比如:getcontentlength 大小 creationdate 创建时间
|
||||
func ReadPropfind(reader io.Reader) (propfind *Propfind) {
|
||||
propfind = &Propfind{}
|
||||
|
||||
c := CountingReader{reader: reader}
|
||||
if err := xml.NewDecoder(&c).Decode(&propfind); err != nil {
|
||||
if err == io.EOF {
|
||||
if c.n == 0 {
|
||||
// An empty body means to propfind allprop.
|
||||
// http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND
|
||||
return &Propfind{Allprop: new(struct{})}
|
||||
}
|
||||
err = errInvalidPropfind
|
||||
}
|
||||
|
||||
panic(result.BadRequest(err.Error()))
|
||||
}
|
||||
|
||||
if propfind.Allprop == nil && propfind.Include != nil {
|
||||
panic(result.BadRequest(errInvalidPropfind.Error()))
|
||||
}
|
||||
if propfind.Allprop != nil && (propfind.Prop != nil || propfind.Propname != nil) {
|
||||
panic(result.BadRequest(errInvalidPropfind.Error()))
|
||||
}
|
||||
if propfind.Prop != nil && propfind.Propname != nil {
|
||||
panic(result.BadRequest(errInvalidPropfind.Error()))
|
||||
}
|
||||
if propfind.Propname == nil && propfind.Allprop == nil && propfind.Prop == nil {
|
||||
panic(result.BadRequest(errInvalidPropfind.Error()))
|
||||
}
|
||||
|
||||
return propfind
|
||||
}
|
||||
|
||||
// Property represents a single DAV resource property as defined in RFC 4918.
|
||||
// See http://www.webdav.org/specs/rfc4918.html#data.model.for.resource.properties
|
||||
type Property struct {
|
||||
// XMLName is the fully qualified name that identifies this property.
|
||||
XMLName xml.Name
|
||||
|
||||
// Lang is an optional xml:lang attribute.
|
||||
Lang string `xml:"xml:lang,attr,omitempty"`
|
||||
|
||||
// InnerXML contains the XML representation of the property value.
|
||||
// See http://www.webdav.org/specs/rfc4918.html#property_values
|
||||
//
|
||||
// Property values of complex type or mixed-content must have fully
|
||||
// expanded XML namespaces or be self-contained with according
|
||||
// XML namespace declarations. They must not rely on any XML
|
||||
// namespace declarations within the scope of the XML document,
|
||||
// even including the DAV: namespace.
|
||||
InnerXML []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// ixmlProperty is the same as the Property type except it holds an xml.Name
|
||||
// instead of an xml.Name.
|
||||
type IxmlProperty struct {
|
||||
XMLName xml.Name
|
||||
Lang string `xml:"xml:lang,attr,omitempty"`
|
||||
InnerXML []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_error
|
||||
// See MultiStatusWriter for the "D:" namespace prefix.
|
||||
type XmlError struct {
|
||||
XMLName xml.Name `xml:"D:error"`
|
||||
InnerXML []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat
|
||||
// See MultiStatusWriter for the "D:" namespace prefix.
|
||||
type SubPropstat struct {
|
||||
Prop []Property `xml:"D:prop>_ignored_"`
|
||||
Status string `xml:"D:status"`
|
||||
Error *XmlError `xml:"D:error"`
|
||||
ResponseDescription string `xml:"D:responsedescription,omitempty"`
|
||||
}
|
||||
|
||||
// ixmlPropstat is the same as the propstat type except it holds an xml.Name
|
||||
// instead of an xml.Name.
|
||||
type IxmlPropstat struct {
|
||||
Prop []IxmlProperty `xml:"D:prop>_ignored_"`
|
||||
Status string `xml:"D:status"`
|
||||
Error *XmlError `xml:"D:error"`
|
||||
ResponseDescription string `xml:"D:responsedescription,omitempty"`
|
||||
}
|
||||
|
||||
// MarshalXML prepends the "D:" namespace prefix on properties in the DAV: namespace
|
||||
// before encoding. See MultiStatusWriter.
|
||||
func (ps SubPropstat) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
// Convert from a propstat to an ixmlPropstat.
|
||||
ixmlPs := IxmlPropstat{
|
||||
Prop: make([]IxmlProperty, len(ps.Prop)),
|
||||
Status: ps.Status,
|
||||
Error: ps.Error,
|
||||
ResponseDescription: ps.ResponseDescription,
|
||||
}
|
||||
for k, prop := range ps.Prop {
|
||||
ixmlPs.Prop[k] = IxmlProperty{
|
||||
XMLName: xml.Name(prop.XMLName),
|
||||
Lang: prop.Lang,
|
||||
InnerXML: prop.InnerXML,
|
||||
}
|
||||
}
|
||||
|
||||
for k, prop := range ixmlPs.Prop {
|
||||
if prop.XMLName.Space == "DAV:" {
|
||||
prop.XMLName = xml.Name{Space: "", Local: "D:" + prop.XMLName.Local}
|
||||
ixmlPs.Prop[k] = prop
|
||||
}
|
||||
}
|
||||
// Distinct type to avoid infinite recursion of MarshalXML.
|
||||
type newpropstat IxmlPropstat
|
||||
return e.EncodeElement(newpropstat(ixmlPs), start)
|
||||
}
|
||||
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_response
|
||||
// See MultiStatusWriter for the "D:" namespace prefix.
|
||||
type Response struct {
|
||||
XMLName xml.Name `xml:"D:response"`
|
||||
Href []string `xml:"D:href"`
|
||||
Propstat []SubPropstat `xml:"D:propstat"`
|
||||
Status string `xml:"D:status,omitempty"`
|
||||
Error *XmlError `xml:"D:error"`
|
||||
ResponseDescription string `xml:"D:responsedescription,omitempty"`
|
||||
}
|
||||
|
||||
// MultistatusWriter marshals one or more Responses into a XML
|
||||
// multistatus response.
|
||||
// See http://www.webdav.org/specs/rfc4918.html#ELEMENT_multistatus
|
||||
// TODO(rsto, mpl): As a workaround, the "D:" namespace prefix, defined as
|
||||
// "DAV:" on this element, is prepended on the nested response, as well as on all
|
||||
// its nested elements. All property names in the DAV: namespace are prefixed as
|
||||
// well. This is because some versions of Mini-Redirector (on windows 7) ignore
|
||||
// elements with a default namespace (no prefixed namespace). A less intrusive fix
|
||||
// should be possible after golang.org/cl/11074. See https://golang.org/issue/11177
|
||||
type MultiStatusWriter struct {
|
||||
// ResponseDescription contains the optional responsedescription
|
||||
// of the multistatus XML element. Only the latest content before
|
||||
// close will be emitted. Empty response descriptions are not
|
||||
// written.
|
||||
ResponseDescription string
|
||||
|
||||
Writer http.ResponseWriter
|
||||
Encoder *xml.Encoder
|
||||
}
|
||||
|
||||
// Write validates and emits a DAV response as part of a multistatus response
|
||||
// element.
|
||||
//
|
||||
// It sets the HTTP status code of its underlying http.ResponseWriter to 207
|
||||
// (Multi-Status) and populates the Content-Type header. If r is the
|
||||
// first, valid response to be written, Write prepends the XML representation
|
||||
// of r with a multistatus tag. Callers must call close after the last response
|
||||
// has been written.
|
||||
func (this *MultiStatusWriter) Write(response *Response) error {
|
||||
switch len(response.Href) {
|
||||
case 0:
|
||||
return errInvalidResponse
|
||||
case 1:
|
||||
if len(response.Propstat) > 0 != (response.Status == "") {
|
||||
return errInvalidResponse
|
||||
}
|
||||
default:
|
||||
if len(response.Propstat) > 0 || response.Status == "" {
|
||||
return errInvalidResponse
|
||||
}
|
||||
}
|
||||
err := this.writeHeader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return this.Encoder.Encode(response)
|
||||
}
|
||||
|
||||
// writeHeader writes a XML multistatus start element on w's underlying
|
||||
// http.ResponseWriter and returns the result of the write operation.
|
||||
// After the first write attempt, writeHeader becomes a no-op.
|
||||
func (this *MultiStatusWriter) writeHeader() error {
|
||||
if this.Encoder != nil {
|
||||
return nil
|
||||
}
|
||||
this.Writer.Header().Add("Content-Type", "text/xml; charset=utf-8")
|
||||
this.Writer.WriteHeader(StatusMulti)
|
||||
_, err := fmt.Fprintf(this.Writer, `<?xml version="1.0" encoding="UTF-8"?>`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
this.Encoder = xml.NewEncoder(this.Writer)
|
||||
return this.Encoder.EncodeToken(xml.StartElement{
|
||||
Name: xml.Name{
|
||||
Space: "DAV:",
|
||||
Local: "multistatus",
|
||||
},
|
||||
Attr: []xml.Attr{{
|
||||
Name: xml.Name{Space: "xmlns", Local: "D"},
|
||||
Value: "DAV:",
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
// Close completes the marshalling of the multistatus response. It returns
|
||||
// an error if the multistatus response could not be completed. If both the
|
||||
// return value and field Encoder of w are nil, then no multistatus response has
|
||||
// been written.
|
||||
func (this *MultiStatusWriter) Close() error {
|
||||
if this.Encoder == nil {
|
||||
return nil
|
||||
}
|
||||
var end []xml.Token
|
||||
if this.ResponseDescription != "" {
|
||||
name := xml.Name{Space: "DAV:", Local: "responsedescription"}
|
||||
end = append(end,
|
||||
xml.StartElement{Name: name},
|
||||
xml.CharData(this.ResponseDescription),
|
||||
xml.EndElement{Name: name},
|
||||
)
|
||||
}
|
||||
end = append(end, xml.EndElement{
|
||||
Name: xml.Name{Space: "DAV:", Local: "multistatus"},
|
||||
})
|
||||
for _, t := range end {
|
||||
err := this.Encoder.EncodeToken(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return this.Encoder.Flush()
|
||||
}
|
||||
|
||||
var xmlLangName = xml.Name{Space: "http://www.w3.org/XML/1998/namespace", Local: "lang"}
|
||||
|
||||
func xmlLang(s xml.StartElement, d string) string {
|
||||
for _, attr := range s.Attr {
|
||||
if attr.Name == xmlLangName {
|
||||
return attr.Value
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
type XmlValue []byte
|
||||
|
||||
func (v *XmlValue) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
// The XML value of a property can be arbitrary, mixed-content XML.
|
||||
// To make sure that the unmarshalled value contains all required
|
||||
// namespaces, we encode all the property value XML tokens into a
|
||||
// buffer. This forces the encoder to redeclare any used namespaces.
|
||||
var b bytes.Buffer
|
||||
e := xml.NewEncoder(&b)
|
||||
for {
|
||||
t, err := next(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e, ok := t.(xml.EndElement); ok && e.Name == start.Name {
|
||||
break
|
||||
}
|
||||
if err = e.EncodeToken(t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err := e.Flush()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*v = b.Bytes()
|
||||
return nil
|
||||
}
|
||||
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_prop (for proppatch)
|
||||
type ProppatchProps []Property
|
||||
|
||||
// UnmarshalXML appends the property names and values enclosed within start
|
||||
// to ps.
|
||||
//
|
||||
// An xml:lang attribute that is defined either on the DAV:prop or property
|
||||
// name XML element is propagated to the property's Lang field.
|
||||
//
|
||||
// UnmarshalXML returns an error if start does not contain any properties or if
|
||||
// property values contain syntactically incorrect XML.
|
||||
func (ps *ProppatchProps) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
lang := xmlLang(start, "")
|
||||
for {
|
||||
t, err := next(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch elem := t.(type) {
|
||||
case xml.EndElement:
|
||||
if len(*ps) == 0 {
|
||||
return fmt.Errorf("%s must not be empty", start.Name.Local)
|
||||
}
|
||||
return nil
|
||||
case xml.StartElement:
|
||||
p := Property{
|
||||
XMLName: xml.Name(t.(xml.StartElement).Name),
|
||||
Lang: xmlLang(t.(xml.StartElement), lang),
|
||||
}
|
||||
err = d.DecodeElement(((*XmlValue)(&p.InnerXML)), &elem)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*ps = append(*ps, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_set
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_remove
|
||||
type SetRemove struct {
|
||||
XMLName xml.Name
|
||||
Lang string `xml:"xml:lang,attr,omitempty"`
|
||||
Prop ProppatchProps `xml:"DAV: prop"`
|
||||
}
|
||||
|
||||
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propertyupdate
|
||||
type PropertyUpdate struct {
|
||||
XMLName xml.Name `xml:"DAV: propertyupdate"`
|
||||
Lang string `xml:"xml:lang,attr,omitempty"`
|
||||
SetRemove []SetRemove `xml:",any"`
|
||||
}
|
1223
code/dav/xml/marshal.go
Normal file
1223
code/dav/xml/marshal.go
Normal file
File diff suppressed because it is too large
Load Diff
692
code/dav/xml/read.go
Normal file
692
code/dav/xml/read.go
Normal file
@ -0,0 +1,692 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package xml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BUG(rsc): Mapping between XML elements and data structures is inherently flawed:
|
||||
// an XML element is an order-dependent collection of anonymous
|
||||
// values, while a data structure is an order-independent collection
|
||||
// of named values.
|
||||
// See package json for a textual representation more suitable
|
||||
// to data structures.
|
||||
|
||||
// Unmarshal parses the XML-encoded data and stores the result in
|
||||
// the value pointed to by v, which must be an arbitrary struct,
|
||||
// slice, or string. Well-formed data that does not fit into v is
|
||||
// discarded.
|
||||
//
|
||||
// Because Unmarshal uses the reflect package, it can only assign
|
||||
// to exported (upper case) fields. Unmarshal uses a case-sensitive
|
||||
// comparison to match XML element names to tag values and struct
|
||||
// field names.
|
||||
//
|
||||
// Unmarshal maps an XML element to a struct using the following rules.
|
||||
// In the rules, the tag of a field refers to the value associated with the
|
||||
// key 'xml' in the struct field's tag (see the example above).
|
||||
//
|
||||
// * If the struct has a field of type []byte or string with tag
|
||||
// ",innerxml", Unmarshal accumulates the raw XML nested inside the
|
||||
// element in that field. The rest of the rules still apply.
|
||||
//
|
||||
// * If the struct has a field named XMLName of type xml.Name,
|
||||
// Unmarshal records the element name in that field.
|
||||
//
|
||||
// * If the XMLName field has an associated tag of the form
|
||||
// "name" or "namespace-URL name", the XML element must have
|
||||
// the given name (and, optionally, name space) or else Unmarshal
|
||||
// returns an error.
|
||||
//
|
||||
// * If the XML element has an attribute whose name matches a
|
||||
// struct field name with an associated tag containing ",attr" or
|
||||
// the explicit name in a struct field tag of the form "name,attr",
|
||||
// Unmarshal records the attribute value in that field.
|
||||
//
|
||||
// * If the XML element contains character data, that data is
|
||||
// accumulated in the first struct field that has tag ",chardata".
|
||||
// The struct field may have type []byte or string.
|
||||
// If there is no such field, the character data is discarded.
|
||||
//
|
||||
// * If the XML element contains comments, they are accumulated in
|
||||
// the first struct field that has tag ",comment". The struct
|
||||
// field may have type []byte or string. If there is no such
|
||||
// field, the comments are discarded.
|
||||
//
|
||||
// * If the XML element contains a sub-element whose name matches
|
||||
// the prefix of a tag formatted as "a" or "a>b>c", unmarshal
|
||||
// will descend into the XML structure looking for elements with the
|
||||
// given names, and will map the innermost elements to that struct
|
||||
// field. A tag starting with ">" is equivalent to one starting
|
||||
// with the field name followed by ">".
|
||||
//
|
||||
// * If the XML element contains a sub-element whose name matches
|
||||
// a struct field's XMLName tag and the struct field has no
|
||||
// explicit name tag as per the previous rule, unmarshal maps
|
||||
// the sub-element to that struct field.
|
||||
//
|
||||
// * If the XML element contains a sub-element whose name matches a
|
||||
// field without any mode flags (",attr", ",chardata", etc), Unmarshal
|
||||
// maps the sub-element to that struct field.
|
||||
//
|
||||
// * If the XML element contains a sub-element that hasn't matched any
|
||||
// of the above rules and the struct has a field with tag ",any",
|
||||
// unmarshal maps the sub-element to that struct field.
|
||||
//
|
||||
// * An anonymous struct field is handled as if the fields of its
|
||||
// value were part of the outer struct.
|
||||
//
|
||||
// * A struct field with tag "-" is never unmarshalled into.
|
||||
//
|
||||
// Unmarshal maps an XML element to a string or []byte by saving the
|
||||
// concatenation of that element's character data in the string or
|
||||
// []byte. The saved []byte is never nil.
|
||||
//
|
||||
// Unmarshal maps an attribute value to a string or []byte by saving
|
||||
// the value in the string or slice.
|
||||
//
|
||||
// Unmarshal maps an XML element to a slice by extending the length of
|
||||
// the slice and mapping the element to the newly created value.
|
||||
//
|
||||
// Unmarshal maps an XML element or attribute value to a bool by
|
||||
// setting it to the boolean value represented by the string.
|
||||
//
|
||||
// Unmarshal maps an XML element or attribute value to an integer or
|
||||
// floating-point field by setting the field to the result of
|
||||
// interpreting the string value in decimal. There is no check for
|
||||
// overflow.
|
||||
//
|
||||
// Unmarshal maps an XML element to an xml.Name by recording the
|
||||
// element name.
|
||||
//
|
||||
// Unmarshal maps an XML element to a pointer by setting the pointer
|
||||
// to a freshly allocated value and then mapping the element to that value.
|
||||
//
|
||||
func Unmarshal(data []byte, v interface{}) error {
|
||||
return NewDecoder(bytes.NewReader(data)).Decode(v)
|
||||
}
|
||||
|
||||
// Decode works like xml.Unmarshal, except it reads the decoder
|
||||
// stream to find the start element.
|
||||
func (d *Decoder) Decode(v interface{}) error {
|
||||
return d.DecodeElement(v, nil)
|
||||
}
|
||||
|
||||
// DecodeElement works like xml.Unmarshal except that it takes
|
||||
// a pointer to the start XML element to decode into v.
|
||||
// It is useful when a client reads some raw XML tokens itself
|
||||
// but also wants to defer to Unmarshal for some elements.
|
||||
func (d *Decoder) DecodeElement(v interface{}, start *StartElement) error {
|
||||
val := reflect.ValueOf(v)
|
||||
if val.Kind() != reflect.Ptr {
|
||||
return errors.New("non-pointer passed to Unmarshal")
|
||||
}
|
||||
return d.unmarshal(val.Elem(), start)
|
||||
}
|
||||
|
||||
// An UnmarshalError represents an error in the unmarshalling process.
|
||||
type UnmarshalError string
|
||||
|
||||
func (e UnmarshalError) Error() string { return string(e) }
|
||||
|
||||
// Unmarshaler is the interface implemented by objects that can unmarshal
|
||||
// an XML element description of themselves.
|
||||
//
|
||||
// UnmarshalXML decodes a single XML element
|
||||
// beginning with the given start element.
|
||||
// If it returns an error, the outer call to Unmarshal stops and
|
||||
// returns that error.
|
||||
// UnmarshalXML must consume exactly one XML element.
|
||||
// One common implementation strategy is to unmarshal into
|
||||
// a separate value with a layout matching the expected XML
|
||||
// using d.DecodeElement, and then to copy the data from
|
||||
// that value into the receiver.
|
||||
// Another common strategy is to use d.Token to process the
|
||||
// XML object one token at a time.
|
||||
// UnmarshalXML may not use d.RawToken.
|
||||
type Unmarshaler interface {
|
||||
UnmarshalXML(d *Decoder, start StartElement) error
|
||||
}
|
||||
|
||||
// UnmarshalerAttr is the interface implemented by objects that can unmarshal
|
||||
// an XML attribute description of themselves.
|
||||
//
|
||||
// UnmarshalXMLAttr decodes a single XML attribute.
|
||||
// If it returns an error, the outer call to Unmarshal stops and
|
||||
// returns that error.
|
||||
// UnmarshalXMLAttr is used only for struct fields with the
|
||||
// "attr" option in the field tag.
|
||||
type UnmarshalerAttr interface {
|
||||
UnmarshalXMLAttr(attr Attr) error
|
||||
}
|
||||
|
||||
// receiverType returns the receiver type to use in an expression like "%s.MethodName".
|
||||
func receiverType(val interface{}) string {
|
||||
t := reflect.TypeOf(val)
|
||||
if t.Name() != "" {
|
||||
return t.String()
|
||||
}
|
||||
return "(" + t.String() + ")"
|
||||
}
|
||||
|
||||
// unmarshalInterface unmarshals a single XML element into val.
|
||||
// start is the opening tag of the element.
|
||||
func (p *Decoder) unmarshalInterface(val Unmarshaler, start *StartElement) error {
|
||||
// Record that decoder must stop at end tag corresponding to start.
|
||||
p.pushEOF()
|
||||
|
||||
p.unmarshalDepth++
|
||||
err := val.UnmarshalXML(p, *start)
|
||||
p.unmarshalDepth--
|
||||
if err != nil {
|
||||
p.popEOF()
|
||||
return err
|
||||
}
|
||||
|
||||
if !p.popEOF() {
|
||||
return fmt.Errorf("xml: %s.UnmarshalXML did not consume entire <%s> element", receiverType(val), start.Name.Local)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// unmarshalTextInterface unmarshals a single XML element into val.
|
||||
// The chardata contained in the element (but not its children)
|
||||
// is passed to the text unmarshaler.
|
||||
func (p *Decoder) unmarshalTextInterface(val encoding.TextUnmarshaler, start *StartElement) error {
|
||||
var buf []byte
|
||||
depth := 1
|
||||
for depth > 0 {
|
||||
t, err := p.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch t := t.(type) {
|
||||
case CharData:
|
||||
if depth == 1 {
|
||||
buf = append(buf, t...)
|
||||
}
|
||||
case StartElement:
|
||||
depth++
|
||||
case EndElement:
|
||||
depth--
|
||||
}
|
||||
}
|
||||
return val.UnmarshalText(buf)
|
||||
}
|
||||
|
||||
// unmarshalAttr unmarshals a single XML attribute into val.
|
||||
func (p *Decoder) unmarshalAttr(val reflect.Value, attr Attr) error {
|
||||
if val.Kind() == reflect.Ptr {
|
||||
if val.IsNil() {
|
||||
val.Set(reflect.New(val.Type().Elem()))
|
||||
}
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
if val.CanInterface() && val.Type().Implements(unmarshalerAttrType) {
|
||||
// This is an unmarshaler with a non-pointer receiver,
|
||||
// so it's likely to be incorrect, but we do what we're told.
|
||||
return val.Interface().(UnmarshalerAttr).UnmarshalXMLAttr(attr)
|
||||
}
|
||||
if val.CanAddr() {
|
||||
pv := val.Addr()
|
||||
if pv.CanInterface() && pv.Type().Implements(unmarshalerAttrType) {
|
||||
return pv.Interface().(UnmarshalerAttr).UnmarshalXMLAttr(attr)
|
||||
}
|
||||
}
|
||||
|
||||
// Not an UnmarshalerAttr; try encoding.TextUnmarshaler.
|
||||
if val.CanInterface() && val.Type().Implements(textUnmarshalerType) {
|
||||
// This is an unmarshaler with a non-pointer receiver,
|
||||
// so it's likely to be incorrect, but we do what we're told.
|
||||
return val.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(attr.Value))
|
||||
}
|
||||
if val.CanAddr() {
|
||||
pv := val.Addr()
|
||||
if pv.CanInterface() && pv.Type().Implements(textUnmarshalerType) {
|
||||
return pv.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(attr.Value))
|
||||
}
|
||||
}
|
||||
|
||||
copyValue(val, []byte(attr.Value))
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
unmarshalerType = reflect.TypeOf((*Unmarshaler)(nil)).Elem()
|
||||
unmarshalerAttrType = reflect.TypeOf((*UnmarshalerAttr)(nil)).Elem()
|
||||
textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
|
||||
)
|
||||
|
||||
// Unmarshal a single XML element into val.
|
||||
func (p *Decoder) unmarshal(val reflect.Value, start *StartElement) error {
|
||||
// Find start element if we need it.
|
||||
if start == nil {
|
||||
for {
|
||||
tok, err := p.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if t, ok := tok.(StartElement); ok {
|
||||
start = &t
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load value from interface, but only if the result will be
|
||||
// usefully addressable.
|
||||
if val.Kind() == reflect.Interface && !val.IsNil() {
|
||||
e := val.Elem()
|
||||
if e.Kind() == reflect.Ptr && !e.IsNil() {
|
||||
val = e
|
||||
}
|
||||
}
|
||||
|
||||
if val.Kind() == reflect.Ptr {
|
||||
if val.IsNil() {
|
||||
val.Set(reflect.New(val.Type().Elem()))
|
||||
}
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
if val.CanInterface() && val.Type().Implements(unmarshalerType) {
|
||||
// This is an unmarshaler with a non-pointer receiver,
|
||||
// so it's likely to be incorrect, but we do what we're told.
|
||||
return p.unmarshalInterface(val.Interface().(Unmarshaler), start)
|
||||
}
|
||||
|
||||
if val.CanAddr() {
|
||||
pv := val.Addr()
|
||||
if pv.CanInterface() && pv.Type().Implements(unmarshalerType) {
|
||||
return p.unmarshalInterface(pv.Interface().(Unmarshaler), start)
|
||||
}
|
||||
}
|
||||
|
||||
if val.CanInterface() && val.Type().Implements(textUnmarshalerType) {
|
||||
return p.unmarshalTextInterface(val.Interface().(encoding.TextUnmarshaler), start)
|
||||
}
|
||||
|
||||
if val.CanAddr() {
|
||||
pv := val.Addr()
|
||||
if pv.CanInterface() && pv.Type().Implements(textUnmarshalerType) {
|
||||
return p.unmarshalTextInterface(pv.Interface().(encoding.TextUnmarshaler), start)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
data []byte
|
||||
saveData reflect.Value
|
||||
comment []byte
|
||||
saveComment reflect.Value
|
||||
saveXML reflect.Value
|
||||
saveXMLIndex int
|
||||
saveXMLData []byte
|
||||
saveAny reflect.Value
|
||||
sv reflect.Value
|
||||
tinfo *typeInfo
|
||||
err error
|
||||
)
|
||||
|
||||
switch v := val; v.Kind() {
|
||||
default:
|
||||
return errors.New("unknown type " + v.Type().String())
|
||||
|
||||
case reflect.Interface:
|
||||
// TODO: For now, simply ignore the field. In the near
|
||||
// future we may choose to unmarshal the start
|
||||
// element on it, if not nil.
|
||||
return p.Skip()
|
||||
|
||||
case reflect.Slice:
|
||||
typ := v.Type()
|
||||
if typ.Elem().Kind() == reflect.Uint8 {
|
||||
// []byte
|
||||
saveData = v
|
||||
break
|
||||
}
|
||||
|
||||
// Slice of element values.
|
||||
// Grow slice.
|
||||
n := v.Len()
|
||||
if n >= v.Cap() {
|
||||
ncap := 2 * n
|
||||
if ncap < 4 {
|
||||
ncap = 4
|
||||
}
|
||||
new := reflect.MakeSlice(typ, n, ncap)
|
||||
reflect.Copy(new, v)
|
||||
v.Set(new)
|
||||
}
|
||||
v.SetLen(n + 1)
|
||||
|
||||
// Recur to read element into slice.
|
||||
if err := p.unmarshal(v.Index(n), start); err != nil {
|
||||
v.SetLen(n)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
case reflect.Bool, reflect.Float32, reflect.Float64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.String:
|
||||
saveData = v
|
||||
|
||||
case reflect.Struct:
|
||||
typ := v.Type()
|
||||
if typ == nameType {
|
||||
v.Set(reflect.ValueOf(start.Name))
|
||||
break
|
||||
}
|
||||
|
||||
sv = v
|
||||
tinfo, err = getTypeInfo(typ)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate and assign element name.
|
||||
if tinfo.xmlname != nil {
|
||||
finfo := tinfo.xmlname
|
||||
if finfo.name != "" && finfo.name != start.Name.Local {
|
||||
return UnmarshalError("expected element type <" + finfo.name + "> but have <" + start.Name.Local + ">")
|
||||
}
|
||||
if finfo.xmlns != "" && finfo.xmlns != start.Name.Space {
|
||||
e := "expected element <" + finfo.name + "> in name space " + finfo.xmlns + " but have "
|
||||
if start.Name.Space == "" {
|
||||
e += "no name space"
|
||||
} else {
|
||||
e += start.Name.Space
|
||||
}
|
||||
return UnmarshalError(e)
|
||||
}
|
||||
fv := finfo.value(sv)
|
||||
if _, ok := fv.Interface().(Name); ok {
|
||||
fv.Set(reflect.ValueOf(start.Name))
|
||||
}
|
||||
}
|
||||
|
||||
// Assign attributes.
|
||||
// Also, determine whether we need to save character data or comments.
|
||||
for i := range tinfo.fields {
|
||||
finfo := &tinfo.fields[i]
|
||||
switch finfo.flags & fMode {
|
||||
case fAttr:
|
||||
strv := finfo.value(sv)
|
||||
// Look for attribute.
|
||||
for _, a := range start.Attr {
|
||||
if a.Name.Local == finfo.name && (finfo.xmlns == "" || finfo.xmlns == a.Name.Space) {
|
||||
if err := p.unmarshalAttr(strv, a); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case fCharData:
|
||||
if !saveData.IsValid() {
|
||||
saveData = finfo.value(sv)
|
||||
}
|
||||
|
||||
case fComment:
|
||||
if !saveComment.IsValid() {
|
||||
saveComment = finfo.value(sv)
|
||||
}
|
||||
|
||||
case fAny, fAny | fElement:
|
||||
if !saveAny.IsValid() {
|
||||
saveAny = finfo.value(sv)
|
||||
}
|
||||
|
||||
case fInnerXml:
|
||||
if !saveXML.IsValid() {
|
||||
saveXML = finfo.value(sv)
|
||||
if p.saved == nil {
|
||||
saveXMLIndex = 0
|
||||
p.saved = new(bytes.Buffer)
|
||||
} else {
|
||||
saveXMLIndex = p.savedOffset()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find end element.
|
||||
// Process sub-elements along the way.
|
||||
Loop:
|
||||
for {
|
||||
var savedOffset int
|
||||
if saveXML.IsValid() {
|
||||
savedOffset = p.savedOffset()
|
||||
}
|
||||
tok, err := p.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch t := tok.(type) {
|
||||
case StartElement:
|
||||
consumed := false
|
||||
if sv.IsValid() {
|
||||
consumed, err = p.unmarshalPath(tinfo, sv, nil, &t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !consumed && saveAny.IsValid() {
|
||||
consumed = true
|
||||
if err := p.unmarshal(saveAny, &t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if !consumed {
|
||||
if err := p.Skip(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case EndElement:
|
||||
if saveXML.IsValid() {
|
||||
saveXMLData = p.saved.Bytes()[saveXMLIndex:savedOffset]
|
||||
if saveXMLIndex == 0 {
|
||||
p.saved = nil
|
||||
}
|
||||
}
|
||||
break Loop
|
||||
|
||||
case CharData:
|
||||
if saveData.IsValid() {
|
||||
data = append(data, t...)
|
||||
}
|
||||
|
||||
case Comment:
|
||||
if saveComment.IsValid() {
|
||||
comment = append(comment, t...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if saveData.IsValid() && saveData.CanInterface() && saveData.Type().Implements(textUnmarshalerType) {
|
||||
if err := saveData.Interface().(encoding.TextUnmarshaler).UnmarshalText(data); err != nil {
|
||||
return err
|
||||
}
|
||||
saveData = reflect.Value{}
|
||||
}
|
||||
|
||||
if saveData.IsValid() && saveData.CanAddr() {
|
||||
pv := saveData.Addr()
|
||||
if pv.CanInterface() && pv.Type().Implements(textUnmarshalerType) {
|
||||
if err := pv.Interface().(encoding.TextUnmarshaler).UnmarshalText(data); err != nil {
|
||||
return err
|
||||
}
|
||||
saveData = reflect.Value{}
|
||||
}
|
||||
}
|
||||
|
||||
if err := copyValue(saveData, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch t := saveComment; t.Kind() {
|
||||
case reflect.String:
|
||||
t.SetString(string(comment))
|
||||
case reflect.Slice:
|
||||
t.Set(reflect.ValueOf(comment))
|
||||
}
|
||||
|
||||
switch t := saveXML; t.Kind() {
|
||||
case reflect.String:
|
||||
t.SetString(string(saveXMLData))
|
||||
case reflect.Slice:
|
||||
t.Set(reflect.ValueOf(saveXMLData))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyValue(dst reflect.Value, src []byte) (err error) {
|
||||
dst0 := dst
|
||||
|
||||
if dst.Kind() == reflect.Ptr {
|
||||
if dst.IsNil() {
|
||||
dst.Set(reflect.New(dst.Type().Elem()))
|
||||
}
|
||||
dst = dst.Elem()
|
||||
}
|
||||
|
||||
// Save accumulated data.
|
||||
switch dst.Kind() {
|
||||
case reflect.Invalid:
|
||||
// Probably a comment.
|
||||
default:
|
||||
return errors.New("cannot unmarshal into " + dst0.Type().String())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
itmp, err := strconv.ParseInt(string(src), 10, dst.Type().Bits())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dst.SetInt(itmp)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
utmp, err := strconv.ParseUint(string(src), 10, dst.Type().Bits())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dst.SetUint(utmp)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
ftmp, err := strconv.ParseFloat(string(src), dst.Type().Bits())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dst.SetFloat(ftmp)
|
||||
case reflect.Bool:
|
||||
value, err := strconv.ParseBool(strings.TrimSpace(string(src)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dst.SetBool(value)
|
||||
case reflect.String:
|
||||
dst.SetString(string(src))
|
||||
case reflect.Slice:
|
||||
if len(src) == 0 {
|
||||
// non-nil to flag presence
|
||||
src = []byte{}
|
||||
}
|
||||
dst.SetBytes(src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// unmarshalPath walks down an XML structure looking for wanted
|
||||
// paths, and calls unmarshal on them.
|
||||
// The consumed result tells whether XML elements have been consumed
|
||||
// from the Decoder until start's matching end element, or if it's
|
||||
// still untouched because start is uninteresting for sv's fields.
|
||||
func (p *Decoder) unmarshalPath(tinfo *typeInfo, sv reflect.Value, parents []string, start *StartElement) (consumed bool, err error) {
|
||||
recurse := false
|
||||
Loop:
|
||||
for i := range tinfo.fields {
|
||||
finfo := &tinfo.fields[i]
|
||||
if finfo.flags&fElement == 0 || len(finfo.parents) < len(parents) || finfo.xmlns != "" && finfo.xmlns != start.Name.Space {
|
||||
continue
|
||||
}
|
||||
for j := range parents {
|
||||
if parents[j] != finfo.parents[j] {
|
||||
continue Loop
|
||||
}
|
||||
}
|
||||
if len(finfo.parents) == len(parents) && finfo.name == start.Name.Local {
|
||||
// It's a perfect match, unmarshal the field.
|
||||
return true, p.unmarshal(finfo.value(sv), start)
|
||||
}
|
||||
if len(finfo.parents) > len(parents) && finfo.parents[len(parents)] == start.Name.Local {
|
||||
// It's a prefix for the field. Break and recurse
|
||||
// since it's not ok for one field path to be itself
|
||||
// the prefix for another field path.
|
||||
recurse = true
|
||||
|
||||
// We can reuse the same slice as long as we
|
||||
// don't try to append to it.
|
||||
parents = finfo.parents[:len(parents)+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if !recurse {
|
||||
// We have no business with this element.
|
||||
return false, nil
|
||||
}
|
||||
// The element is not a perfect match for any field, but one
|
||||
// or more fields have the path to this element as a parent
|
||||
// prefix. Recurse and attempt to match these.
|
||||
for {
|
||||
var tok Token
|
||||
tok, err = p.Token()
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
switch t := tok.(type) {
|
||||
case StartElement:
|
||||
consumed2, err := p.unmarshalPath(tinfo, sv, parents, &t)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
if !consumed2 {
|
||||
if err := p.Skip(); err != nil {
|
||||
return true, err
|
||||
}
|
||||
}
|
||||
case EndElement:
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip reads tokens until it has consumed the end element
|
||||
// matching the most recent start element already consumed.
|
||||
// It recurs if it encounters a start element, so it can be used to
|
||||
// skip nested structures.
|
||||
// It returns nil if it finds an end element matching the start
|
||||
// element; otherwise it returns an error describing the problem.
|
||||
func (d *Decoder) Skip() error {
|
||||
for {
|
||||
tok, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch tok.(type) {
|
||||
case StartElement:
|
||||
if err := d.Skip(); err != nil {
|
||||
return err
|
||||
}
|
||||
case EndElement:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
371
code/dav/xml/typeinfo.go
Normal file
371
code/dav/xml/typeinfo.go
Normal file
@ -0,0 +1,371 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package xml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// typeInfo holds details for the xml representation of a type.
|
||||
type typeInfo struct {
|
||||
xmlname *fieldInfo
|
||||
fields []fieldInfo
|
||||
}
|
||||
|
||||
// fieldInfo holds details for the xml representation of a single field.
|
||||
type fieldInfo struct {
|
||||
idx []int
|
||||
name string
|
||||
xmlns string
|
||||
flags fieldFlags
|
||||
parents []string
|
||||
}
|
||||
|
||||
type fieldFlags int
|
||||
|
||||
const (
|
||||
fElement fieldFlags = 1 << iota
|
||||
fAttr
|
||||
fCharData
|
||||
fInnerXml
|
||||
fComment
|
||||
fAny
|
||||
|
||||
fOmitEmpty
|
||||
|
||||
fMode = fElement | fAttr | fCharData | fInnerXml | fComment | fAny
|
||||
)
|
||||
|
||||
var tinfoMap = make(map[reflect.Type]*typeInfo)
|
||||
var tinfoLock sync.RWMutex
|
||||
|
||||
var nameType = reflect.TypeOf(Name{})
|
||||
|
||||
// getTypeInfo returns the typeInfo structure with details necessary
|
||||
// for marshalling and unmarshalling typ.
|
||||
func getTypeInfo(typ reflect.Type) (*typeInfo, error) {
|
||||
tinfoLock.RLock()
|
||||
tinfo, ok := tinfoMap[typ]
|
||||
tinfoLock.RUnlock()
|
||||
if ok {
|
||||
return tinfo, nil
|
||||
}
|
||||
tinfo = &typeInfo{}
|
||||
if typ.Kind() == reflect.Struct && typ != nameType {
|
||||
n := typ.NumField()
|
||||
for i := 0; i < n; i++ {
|
||||
f := typ.Field(i)
|
||||
if f.PkgPath != "" || f.Tag.Get("xml") == "-" {
|
||||
continue // Private field
|
||||
}
|
||||
|
||||
// For embedded structs, embed its fields.
|
||||
if f.Anonymous {
|
||||
t := f.Type
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
if t.Kind() == reflect.Struct {
|
||||
inner, err := getTypeInfo(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tinfo.xmlname == nil {
|
||||
tinfo.xmlname = inner.xmlname
|
||||
}
|
||||
for _, finfo := range inner.fields {
|
||||
finfo.idx = append([]int{i}, finfo.idx...)
|
||||
if err := addFieldInfo(typ, tinfo, &finfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
finfo, err := structFieldInfo(typ, &f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if f.Name == "XMLName" {
|
||||
tinfo.xmlname = finfo
|
||||
continue
|
||||
}
|
||||
|
||||
// Add the field if it doesn't conflict with other fields.
|
||||
if err := addFieldInfo(typ, tinfo, finfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
tinfoLock.Lock()
|
||||
tinfoMap[typ] = tinfo
|
||||
tinfoLock.Unlock()
|
||||
return tinfo, nil
|
||||
}
|
||||
|
||||
// structFieldInfo builds and returns a fieldInfo for f.
|
||||
func structFieldInfo(typ reflect.Type, f *reflect.StructField) (*fieldInfo, error) {
|
||||
finfo := &fieldInfo{idx: f.Index}
|
||||
|
||||
// Split the tag from the xml namespace if necessary.
|
||||
tag := f.Tag.Get("xml")
|
||||
if i := strings.Index(tag, " "); i >= 0 {
|
||||
finfo.xmlns, tag = tag[:i], tag[i+1:]
|
||||
}
|
||||
|
||||
// Parse flags.
|
||||
tokens := strings.Split(tag, ",")
|
||||
if len(tokens) == 1 {
|
||||
finfo.flags = fElement
|
||||
} else {
|
||||
tag = tokens[0]
|
||||
for _, flag := range tokens[1:] {
|
||||
switch flag {
|
||||
case "attr":
|
||||
finfo.flags |= fAttr
|
||||
case "chardata":
|
||||
finfo.flags |= fCharData
|
||||
case "innerxml":
|
||||
finfo.flags |= fInnerXml
|
||||
case "comment":
|
||||
finfo.flags |= fComment
|
||||
case "any":
|
||||
finfo.flags |= fAny
|
||||
case "omitempty":
|
||||
finfo.flags |= fOmitEmpty
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the flags used.
|
||||
valid := true
|
||||
switch mode := finfo.flags & fMode; mode {
|
||||
case 0:
|
||||
finfo.flags |= fElement
|
||||
case fAttr, fCharData, fInnerXml, fComment, fAny:
|
||||
if f.Name == "XMLName" || tag != "" && mode != fAttr {
|
||||
valid = false
|
||||
}
|
||||
default:
|
||||
// This will also catch multiple modes in a single field.
|
||||
valid = false
|
||||
}
|
||||
if finfo.flags&fMode == fAny {
|
||||
finfo.flags |= fElement
|
||||
}
|
||||
if finfo.flags&fOmitEmpty != 0 && finfo.flags&(fElement|fAttr) == 0 {
|
||||
valid = false
|
||||
}
|
||||
if !valid {
|
||||
return nil, fmt.Errorf("xml: invalid tag in field %s of type %s: %q",
|
||||
f.Name, typ, f.Tag.Get("xml"))
|
||||
}
|
||||
}
|
||||
|
||||
// Use of xmlns without a name is not allowed.
|
||||
if finfo.xmlns != "" && tag == "" {
|
||||
return nil, fmt.Errorf("xml: namespace without name in field %s of type %s: %q",
|
||||
f.Name, typ, f.Tag.Get("xml"))
|
||||
}
|
||||
|
||||
if f.Name == "XMLName" {
|
||||
// The XMLName field records the XML element name. Don't
|
||||
// process it as usual because its name should default to
|
||||
// empty rather than to the field name.
|
||||
finfo.name = tag
|
||||
return finfo, nil
|
||||
}
|
||||
|
||||
if tag == "" {
|
||||
// If the name part of the tag is completely empty, get
|
||||
// default from XMLName of underlying struct if feasible,
|
||||
// or field name otherwise.
|
||||
if xmlname := lookupXMLName(f.Type); xmlname != nil {
|
||||
finfo.xmlns, finfo.name = xmlname.xmlns, xmlname.name
|
||||
} else {
|
||||
finfo.name = f.Name
|
||||
}
|
||||
return finfo, nil
|
||||
}
|
||||
|
||||
if finfo.xmlns == "" && finfo.flags&fAttr == 0 {
|
||||
// If it's an element no namespace specified, get the default
|
||||
// from the XMLName of enclosing struct if possible.
|
||||
if xmlname := lookupXMLName(typ); xmlname != nil {
|
||||
finfo.xmlns = xmlname.xmlns
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare field name and parents.
|
||||
parents := strings.Split(tag, ">")
|
||||
if parents[0] == "" {
|
||||
parents[0] = f.Name
|
||||
}
|
||||
if parents[len(parents)-1] == "" {
|
||||
return nil, fmt.Errorf("xml: trailing '>' in field %s of type %s", f.Name, typ)
|
||||
}
|
||||
finfo.name = parents[len(parents)-1]
|
||||
if len(parents) > 1 {
|
||||
if (finfo.flags & fElement) == 0 {
|
||||
return nil, fmt.Errorf("xml: %s chain not valid with %s flag", tag, strings.Join(tokens[1:], ","))
|
||||
}
|
||||
finfo.parents = parents[:len(parents)-1]
|
||||
}
|
||||
|
||||
// If the field type has an XMLName field, the names must match
|
||||
// so that the behavior of both marshalling and unmarshalling
|
||||
// is straightforward and unambiguous.
|
||||
if finfo.flags&fElement != 0 {
|
||||
ftyp := f.Type
|
||||
xmlname := lookupXMLName(ftyp)
|
||||
if xmlname != nil && xmlname.name != finfo.name {
|
||||
return nil, fmt.Errorf("xml: name %q in tag of %s.%s conflicts with name %q in %s.XMLName",
|
||||
finfo.name, typ, f.Name, xmlname.name, ftyp)
|
||||
}
|
||||
}
|
||||
return finfo, nil
|
||||
}
|
||||
|
||||
// lookupXMLName returns the fieldInfo for typ's XMLName field
|
||||
// in case it exists and has a valid xml field tag, otherwise
|
||||
// it returns nil.
|
||||
func lookupXMLName(typ reflect.Type) (xmlname *fieldInfo) {
|
||||
for typ.Kind() == reflect.Ptr {
|
||||
typ = typ.Elem()
|
||||
}
|
||||
if typ.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
for i, n := 0, typ.NumField(); i < n; i++ {
|
||||
f := typ.Field(i)
|
||||
if f.Name != "XMLName" {
|
||||
continue
|
||||
}
|
||||
finfo, err := structFieldInfo(typ, &f)
|
||||
if finfo.name != "" && err == nil {
|
||||
return finfo
|
||||
}
|
||||
// Also consider errors as a non-existent field tag
|
||||
// and let getTypeInfo itself report the error.
|
||||
break
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a <= b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// addFieldInfo adds finfo to tinfo.fields if there are no
|
||||
// conflicts, or if conflicts arise from previous fields that were
|
||||
// obtained from deeper embedded structures than finfo. In the latter
|
||||
// case, the conflicting entries are dropped.
|
||||
// A conflict occurs when the path (parent + name) to a field is
|
||||
// itself a prefix of another path, or when two paths match exactly.
|
||||
// It is okay for field paths to share a common, shorter prefix.
|
||||
func addFieldInfo(typ reflect.Type, tinfo *typeInfo, newf *fieldInfo) error {
|
||||
var conflicts []int
|
||||
Loop:
|
||||
// First, figure all conflicts. Most working code will have none.
|
||||
for i := range tinfo.fields {
|
||||
oldf := &tinfo.fields[i]
|
||||
if oldf.flags&fMode != newf.flags&fMode {
|
||||
continue
|
||||
}
|
||||
if oldf.xmlns != "" && newf.xmlns != "" && oldf.xmlns != newf.xmlns {
|
||||
continue
|
||||
}
|
||||
minl := min(len(newf.parents), len(oldf.parents))
|
||||
for p := 0; p < minl; p++ {
|
||||
if oldf.parents[p] != newf.parents[p] {
|
||||
continue Loop
|
||||
}
|
||||
}
|
||||
if len(oldf.parents) > len(newf.parents) {
|
||||
if oldf.parents[len(newf.parents)] == newf.name {
|
||||
conflicts = append(conflicts, i)
|
||||
}
|
||||
} else if len(oldf.parents) < len(newf.parents) {
|
||||
if newf.parents[len(oldf.parents)] == oldf.name {
|
||||
conflicts = append(conflicts, i)
|
||||
}
|
||||
} else {
|
||||
if newf.name == oldf.name {
|
||||
conflicts = append(conflicts, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Without conflicts, add the new field and return.
|
||||
if conflicts == nil {
|
||||
tinfo.fields = append(tinfo.fields, *newf)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If any conflict is shallower, ignore the new field.
|
||||
// This matches the Go field resolution on embedding.
|
||||
for _, i := range conflicts {
|
||||
if len(tinfo.fields[i].idx) < len(newf.idx) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, if any of them is at the same depth level, it's an error.
|
||||
for _, i := range conflicts {
|
||||
oldf := &tinfo.fields[i]
|
||||
if len(oldf.idx) == len(newf.idx) {
|
||||
f1 := typ.FieldByIndex(oldf.idx)
|
||||
f2 := typ.FieldByIndex(newf.idx)
|
||||
return &TagPathError{typ, f1.Name, f1.Tag.Get("xml"), f2.Name, f2.Tag.Get("xml")}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, the new field is shallower, and thus takes precedence,
|
||||
// so drop the conflicting fields from tinfo and append the new one.
|
||||
for c := len(conflicts) - 1; c >= 0; c-- {
|
||||
i := conflicts[c]
|
||||
copy(tinfo.fields[i:], tinfo.fields[i+1:])
|
||||
tinfo.fields = tinfo.fields[:len(tinfo.fields)-1]
|
||||
}
|
||||
tinfo.fields = append(tinfo.fields, *newf)
|
||||
return nil
|
||||
}
|
||||
|
||||
// A TagPathError represents an error in the unmarshalling process
|
||||
// caused by the use of field tags with conflicting paths.
|
||||
type TagPathError struct {
|
||||
Struct reflect.Type
|
||||
Field1, Tag1 string
|
||||
Field2, Tag2 string
|
||||
}
|
||||
|
||||
func (e *TagPathError) Error() string {
|
||||
return fmt.Sprintf("%s field %q with tag %q conflicts with field %q with tag %q", e.Struct, e.Field1, e.Tag1, e.Field2, e.Tag2)
|
||||
}
|
||||
|
||||
// value returns v's field value corresponding to finfo.
|
||||
// It's equivalent to v.FieldByIndex(finfo.idx), but initializes
|
||||
// and dereferences pointers as necessary.
|
||||
func (finfo *fieldInfo) value(v reflect.Value) reflect.Value {
|
||||
for i, x := range finfo.idx {
|
||||
if i > 0 {
|
||||
t := v.Type()
|
||||
if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct {
|
||||
if v.IsNil() {
|
||||
v.Set(reflect.New(v.Type().Elem()))
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
}
|
||||
v = v.Field(x)
|
||||
}
|
||||
return v
|
||||
}
|
1998
code/dav/xml/xml.go
Normal file
1998
code/dav/xml/xml.go
Normal file
File diff suppressed because it is too large
Load Diff
174
code/dav_controller.go
Normal file
174
code/dav_controller.go
Normal file
@ -0,0 +1,174 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"tank/code/result"
|
||||
"tank/code/tool"
|
||||
)
|
||||
|
||||
/**
|
||||
*
|
||||
* WebDav协议文档
|
||||
* https://tools.ietf.org/html/rfc4918
|
||||
* http://www.webdav.org/specs/rfc4918.html
|
||||
*
|
||||
*/
|
||||
|
||||
type DavController struct {
|
||||
BaseController
|
||||
uploadTokenDao *UploadTokenDao
|
||||
downloadTokenDao *DownloadTokenDao
|
||||
matterDao *MatterDao
|
||||
matterService *MatterService
|
||||
imageCacheDao *ImageCacheDao
|
||||
imageCacheService *ImageCacheService
|
||||
davService *DavService
|
||||
}
|
||||
|
||||
//初始化方法
|
||||
func (this *DavController) Init() {
|
||||
this.BaseController.Init()
|
||||
|
||||
//手动装填本实例的Bean.
|
||||
b := CONTEXT.GetBean(this.uploadTokenDao)
|
||||
if c, ok := b.(*UploadTokenDao); ok {
|
||||
this.uploadTokenDao = c
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.downloadTokenDao)
|
||||
if c, ok := b.(*DownloadTokenDao); ok {
|
||||
this.downloadTokenDao = c
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.matterDao)
|
||||
if c, ok := b.(*MatterDao); ok {
|
||||
this.matterDao = c
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.matterService)
|
||||
if c, ok := b.(*MatterService); ok {
|
||||
this.matterService = c
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.imageCacheDao)
|
||||
if c, ok := b.(*ImageCacheDao); ok {
|
||||
this.imageCacheDao = c
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.imageCacheService)
|
||||
if c, ok := b.(*ImageCacheService); ok {
|
||||
this.imageCacheService = c
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.davService)
|
||||
if c, ok := b.(*DavService); ok {
|
||||
this.davService = c
|
||||
}
|
||||
}
|
||||
|
||||
//通过BasicAuth的方式授权。
|
||||
func (this *DavController) CheckCurrentUser(writer http.ResponseWriter, request *http.Request) *User {
|
||||
|
||||
username, password, ok := request.BasicAuth()
|
||||
if !ok {
|
||||
//要求前端使用Basic的形式授权
|
||||
writer.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
|
||||
panic(result.ConstWebResult(result.CODE_WRAPPER_LOGIN))
|
||||
|
||||
}
|
||||
|
||||
user := this.userDao.FindByUsername(username)
|
||||
if user == nil {
|
||||
panic(result.BadRequest("邮箱或密码错误"))
|
||||
} else {
|
||||
if !tool.MatchBcrypt(password, user.Password) {
|
||||
panic(result.BadRequest("邮箱或密码错误"))
|
||||
}
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
//注册自己的路由。
|
||||
func (this *DavController) RegisterRoutes() map[string]func(writer http.ResponseWriter, request *http.Request) {
|
||||
|
||||
routeMap := make(map[string]func(writer http.ResponseWriter, request *http.Request))
|
||||
|
||||
return routeMap
|
||||
}
|
||||
|
||||
//处理一些特殊的接口,比如参数包含在路径中,一般情况下,controller不将参数放在url路径中
|
||||
func (this *DavController) HandleRoutes(writer http.ResponseWriter, request *http.Request) (func(writer http.ResponseWriter, request *http.Request), bool) {
|
||||
|
||||
path := request.URL.Path
|
||||
|
||||
//匹配 /api/dav{subPath}
|
||||
pattern := fmt.Sprintf(`^%s(.*)$`, WEBDAV_PREFFIX)
|
||||
reg := regexp.MustCompile(pattern)
|
||||
strs := reg.FindStringSubmatch(path)
|
||||
if len(strs) == 2 {
|
||||
var f = func(writer http.ResponseWriter, request *http.Request) {
|
||||
subPath := strs[1]
|
||||
//保证subPath不是以/结尾的。
|
||||
//最后多余的/要去掉
|
||||
if strings.HasSuffix(subPath, "/") {
|
||||
subPath = subPath[0 : len(subPath)-1]
|
||||
}
|
||||
this.Index(writer, request, subPath)
|
||||
}
|
||||
return f, true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
//完成系统安装
|
||||
func (this *DavController) Index(writer http.ResponseWriter, request *http.Request, subPath string) {
|
||||
|
||||
/*打印所有HEADER以及请求参数*/
|
||||
|
||||
fmt.Printf("\n------ 请求: %s -- %s ------\n", request.URL, subPath)
|
||||
|
||||
fmt.Printf("\n------Method:------\n")
|
||||
fmt.Println(request.Method)
|
||||
|
||||
fmt.Printf("\n------Header:------\n")
|
||||
for key, value := range request.Header {
|
||||
fmt.Printf("%s = %s\n", key, value)
|
||||
}
|
||||
|
||||
fmt.Printf("\n------请求参数:------\n")
|
||||
for key, value := range request.Form {
|
||||
fmt.Printf("%s = %s\n", key, value)
|
||||
}
|
||||
|
||||
fmt.Printf("\n------Body:------\n")
|
||||
//ioutil.ReadAll 不可重复读,第二次读的时候就什么都没有了。
|
||||
|
||||
bodyBytes, err := ioutil.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
fmt.Println("读取body时出错" + err.Error())
|
||||
}
|
||||
fmt.Println(string(bodyBytes))
|
||||
|
||||
//关闭之后再重新赋值
|
||||
err = request.Body.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
|
||||
fmt.Println("------------------")
|
||||
|
||||
//获取请求者
|
||||
user := this.CheckCurrentUser(writer, request)
|
||||
|
||||
this.davService.HandleDav(writer, request, user, subPath)
|
||||
|
||||
}
|
123
code/dav_model.go
Normal file
123
code/dav_model.go
Normal file
@ -0,0 +1,123 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"tank/code/dav"
|
||||
"tank/code/dav/xml"
|
||||
)
|
||||
|
||||
//访问前缀,这个是特殊入口
|
||||
var WEBDAV_PREFFIX = "/api/dav"
|
||||
|
||||
//动态的文件属性
|
||||
type LiveProp struct {
|
||||
findFn func(user *User, matter *Matter) string
|
||||
dir bool
|
||||
}
|
||||
|
||||
//所有的动态属性定义及其值的获取方式
|
||||
var LivePropMap = map[xml.Name]LiveProp{
|
||||
{Space: "DAV:", Local: "resourcetype"}: {
|
||||
findFn: func(user *User, matter *Matter) string {
|
||||
if matter.Dir {
|
||||
return `<D:collection xmlns:D="DAV:"/>`
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
},
|
||||
dir: true,
|
||||
},
|
||||
{Space: "DAV:", Local: "displayname"}: {
|
||||
findFn: func(user *User, matter *Matter) string {
|
||||
if path.Clean("/"+matter.Name) == "/" {
|
||||
return ""
|
||||
} else {
|
||||
return dav.EscapeXML(matter.Name)
|
||||
}
|
||||
},
|
||||
dir: true,
|
||||
},
|
||||
{Space: "DAV:", Local: "getcontentlength"}: {
|
||||
findFn: func(user *User, matter *Matter) string {
|
||||
return strconv.FormatInt(matter.Size, 10)
|
||||
},
|
||||
dir: false,
|
||||
},
|
||||
{Space: "DAV:", Local: "getlastmodified"}: {
|
||||
findFn: func(user *User, matter *Matter) string {
|
||||
return matter.UpdateTime.UTC().Format(http.TimeFormat)
|
||||
},
|
||||
// http://webdav.org/specs/rfc4918.html#PROPERTY_getlastmodified
|
||||
// suggests that getlastmodified should only apply to GETable
|
||||
// resources, and this package does not support GET on directories.
|
||||
//
|
||||
// Nonetheless, some WebDAV clients expect child directories to be
|
||||
// sortable by getlastmodified date, so this value is true, not false.
|
||||
// See golang.org/issue/15334.
|
||||
dir: true,
|
||||
},
|
||||
{Space: "DAV:", Local: "creationdate"}: {
|
||||
findFn: nil,
|
||||
dir: false,
|
||||
},
|
||||
{Space: "DAV:", Local: "getcontentlanguage"}: {
|
||||
findFn: nil,
|
||||
dir: false,
|
||||
},
|
||||
{Space: "DAV:", Local: "getcontenttype"}: {
|
||||
findFn: func(user *User, matter *Matter) string {
|
||||
if matter.Dir {
|
||||
return ""
|
||||
} else {
|
||||
return dav.EscapeXML(matter.Name)
|
||||
}
|
||||
},
|
||||
dir: false,
|
||||
},
|
||||
{Space: "DAV:", Local: "getetag"}: {
|
||||
findFn: func(user *User, matter *Matter) string {
|
||||
return fmt.Sprintf(`"%x%x"`, matter.UpdateTime.UnixNano(), matter.Size)
|
||||
},
|
||||
// findETag implements ETag as the concatenated hex values of a file's
|
||||
// modification time and size. This is not a reliable synchronization
|
||||
// mechanism for directories, so we do not advertise getetag for DAV
|
||||
// collections.
|
||||
dir: false,
|
||||
},
|
||||
// TODO: The lockdiscovery property requires LockSystem to list the
|
||||
// active locks on a resource.
|
||||
{Space: "DAV:", Local: "lockdiscovery"}: {},
|
||||
{Space: "DAV:", Local: "supportedlock"}: {
|
||||
findFn: func(user *User, matter *Matter) string {
|
||||
return `` +
|
||||
`<D:lockentry xmlns:D="DAV:">` +
|
||||
`<D:lockscope><D:exclusive/></D:lockscope>` +
|
||||
`<D:locktype><D:write/></D:locktype>` +
|
||||
`</D:lockentry>`
|
||||
},
|
||||
dir: true,
|
||||
},
|
||||
{Space: "DAV:", Local: "quota-available-bytes"}: {
|
||||
findFn: func(user *User, matter *Matter) string {
|
||||
var size int64 = 0
|
||||
if user.SizeLimit >= 0 {
|
||||
size = user.SizeLimit
|
||||
} else {
|
||||
//没有限制,默认100G
|
||||
size = 100 * 1024 * 1024 * 1024
|
||||
}
|
||||
return fmt.Sprintf(`%d`, size)
|
||||
},
|
||||
dir: true,
|
||||
},
|
||||
{Space: "DAV:", Local: "quota-used-bytes"}: {
|
||||
findFn: func(user *User, matter *Matter) string {
|
||||
//已使用大小,默认0
|
||||
return fmt.Sprintf(`%d`, 0)
|
||||
},
|
||||
dir: true,
|
||||
},
|
||||
}
|
488
code/dav_service.go
Normal file
488
code/dav_service.go
Normal file
@ -0,0 +1,488 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"tank/code/dav"
|
||||
"tank/code/dav/xml"
|
||||
"tank/code/result"
|
||||
"tank/code/tool"
|
||||
)
|
||||
|
||||
/**
|
||||
*
|
||||
* WebDav协议文档
|
||||
* https://tools.ietf.org/html/rfc4918
|
||||
* 主要参考 golang.org/x/net/webdav
|
||||
* 测试机:http://www.webdav.org/neon/litmus/
|
||||
*/
|
||||
//@Service
|
||||
type DavService struct {
|
||||
Bean
|
||||
matterDao *MatterDao
|
||||
matterService *MatterService
|
||||
}
|
||||
|
||||
//初始化方法
|
||||
func (this *DavService) Init() {
|
||||
this.Bean.Init()
|
||||
|
||||
//手动装填本实例的Bean. 这里必须要用中间变量方可。
|
||||
b := CONTEXT.GetBean(this.matterDao)
|
||||
if b, ok := b.(*MatterDao); ok {
|
||||
this.matterDao = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.matterService)
|
||||
if b, ok := b.(*MatterService); ok {
|
||||
this.matterService = b
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//获取Header头中的Depth值,暂不支持 infinity
|
||||
func (this *DavService) ParseDepth(request *http.Request) int {
|
||||
|
||||
depth := 1
|
||||
if hdr := request.Header.Get("Depth"); hdr != "" {
|
||||
switch hdr {
|
||||
case "0":
|
||||
return 0
|
||||
case "1":
|
||||
return 1
|
||||
case "infinity":
|
||||
return 1
|
||||
}
|
||||
} else {
|
||||
panic(result.BadRequest("必须指定Header Depth"))
|
||||
}
|
||||
return depth
|
||||
}
|
||||
|
||||
func (this *DavService) makePropstatResponse(href string, pstats []dav.Propstat) *dav.Response {
|
||||
resp := dav.Response{
|
||||
Href: []string{(&url.URL{Path: href}).EscapedPath()},
|
||||
Propstat: make([]dav.SubPropstat, 0, len(pstats)),
|
||||
}
|
||||
for _, p := range pstats {
|
||||
var xmlErr *dav.XmlError
|
||||
if p.XMLError != "" {
|
||||
xmlErr = &dav.XmlError{InnerXML: []byte(p.XMLError)}
|
||||
}
|
||||
resp.Propstat = append(resp.Propstat, dav.SubPropstat{
|
||||
Status: fmt.Sprintf("HTTP/1.1 %d %s", p.Status, dav.StatusText(p.Status)),
|
||||
Prop: p.Props,
|
||||
ResponseDescription: p.ResponseDescription,
|
||||
Error: xmlErr,
|
||||
})
|
||||
}
|
||||
return &resp
|
||||
}
|
||||
|
||||
//从一个matter中获取其 []dav.Propstat
|
||||
func (this *DavService) PropstatsFromXmlNames(user *User, matter *Matter, xmlNames []xml.Name) []dav.Propstat {
|
||||
|
||||
propstats := make([]dav.Propstat, 0)
|
||||
|
||||
var properties []dav.Property
|
||||
|
||||
for _, xmlName := range xmlNames {
|
||||
//TODO: deadprops尚未考虑
|
||||
|
||||
// Otherwise, it must either be a live property or we don't know it.
|
||||
if liveProp := LivePropMap[xmlName]; liveProp.findFn != nil && (liveProp.dir || !matter.Dir) {
|
||||
innerXML := liveProp.findFn(user, matter)
|
||||
|
||||
properties = append(properties, dav.Property{
|
||||
XMLName: xmlName,
|
||||
InnerXML: []byte(innerXML),
|
||||
})
|
||||
} else {
|
||||
this.logger.Info("%s的%s无法完成", matter.Path, xmlName.Local)
|
||||
}
|
||||
}
|
||||
|
||||
if len(properties) == 0 {
|
||||
panic(result.BadRequest("请求的属性项无法解析!"))
|
||||
}
|
||||
|
||||
okPropstat := dav.Propstat{Status: http.StatusOK, Props: properties}
|
||||
|
||||
propstats = append(propstats, okPropstat)
|
||||
|
||||
return propstats
|
||||
|
||||
}
|
||||
|
||||
//从一个matter中获取所有的propsNames
|
||||
func (this *DavService) AllPropXmlNames(matter *Matter) []xml.Name {
|
||||
|
||||
pnames := make([]xml.Name, 0)
|
||||
for pn, prop := range LivePropMap {
|
||||
if prop.findFn != nil && (prop.dir || !matter.Dir) {
|
||||
pnames = append(pnames, pn)
|
||||
}
|
||||
}
|
||||
|
||||
return pnames
|
||||
}
|
||||
|
||||
//从一个matter中获取其 []dav.Propstat
|
||||
func (this *DavService) Propstats(user *User, matter *Matter, propfind *dav.Propfind) []dav.Propstat {
|
||||
|
||||
propstats := make([]dav.Propstat, 0)
|
||||
if propfind.Propname != nil {
|
||||
panic(result.BadRequest("propfind.Propname != nil 尚未处理"))
|
||||
} else if propfind.Allprop != nil {
|
||||
|
||||
//TODO: 如果include中还有内容,那么包含进去。
|
||||
xmlNames := this.AllPropXmlNames(matter)
|
||||
|
||||
propstats = this.PropstatsFromXmlNames(user, matter, xmlNames)
|
||||
|
||||
} else {
|
||||
propstats = this.PropstatsFromXmlNames(user, matter, propfind.Prop)
|
||||
}
|
||||
|
||||
return propstats
|
||||
|
||||
}
|
||||
|
||||
//列出文件夹或者目录详情
|
||||
func (this *DavService) HandlePropfind(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
|
||||
|
||||
fmt.Printf("PROPFIND %s\n", subPath)
|
||||
|
||||
//获取请求的层数。暂不支持 infinity
|
||||
depth := this.ParseDepth(request)
|
||||
|
||||
//读取请求参数。按照用户的参数请求返回内容。
|
||||
propfind := dav.ReadPropfind(request.Body)
|
||||
|
||||
//寻找符合条件的matter.
|
||||
//如果是空或者/就是请求根目录
|
||||
matter := this.matterDao.CheckWithRootByPath(subPath, user)
|
||||
|
||||
var matters []*Matter
|
||||
if depth == 0 {
|
||||
matters = []*Matter{matter}
|
||||
} else {
|
||||
// len(matters) == 0 表示该文件夹下面是空文件夹
|
||||
matters = this.matterDao.List(matter.Uuid, user.Uuid, nil)
|
||||
|
||||
//将当前的matter添加到头部
|
||||
matters = append([]*Matter{matter}, matters...)
|
||||
}
|
||||
|
||||
//准备一个输出结果的Writer
|
||||
multiStatusWriter := &dav.MultiStatusWriter{Writer: writer}
|
||||
|
||||
for _, matter := range matters {
|
||||
|
||||
fmt.Printf("处理Matter %s\n", matter.Path)
|
||||
|
||||
propstats := this.Propstats(user, matter, propfind)
|
||||
path := fmt.Sprintf("%s%s", WEBDAV_PREFFIX, matter.Path)
|
||||
response := this.makePropstatResponse(path, propstats)
|
||||
|
||||
err := multiStatusWriter.Write(response)
|
||||
this.PanicError(err)
|
||||
}
|
||||
|
||||
//闭合
|
||||
err := multiStatusWriter.Close()
|
||||
this.PanicError(err)
|
||||
|
||||
fmt.Printf("%v %v \n", subPath, propfind.Prop)
|
||||
|
||||
}
|
||||
|
||||
//请求文件详情(下载)
|
||||
func (this *DavService) HandleGetHeadPost(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
|
||||
|
||||
fmt.Printf("GET %s\n", subPath)
|
||||
|
||||
matter := this.matterDao.CheckWithRootByPath(subPath, user)
|
||||
|
||||
//如果是文件夹,相当于是 Propfind
|
||||
if matter.Dir {
|
||||
this.HandlePropfind(writer, request, user, subPath)
|
||||
return
|
||||
}
|
||||
|
||||
//下载一个文件。
|
||||
this.matterService.DownloadFile(writer, request, matter.AbsolutePath(), matter.Name, false)
|
||||
|
||||
}
|
||||
|
||||
//上传文件
|
||||
func (this *DavService) HandlePut(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
|
||||
|
||||
fmt.Printf("PUT %s\n", subPath)
|
||||
|
||||
filename := tool.GetFilenameOfPath(subPath)
|
||||
dirPath := tool.GetDirOfPath(subPath)
|
||||
|
||||
//寻找符合条件的matter.
|
||||
dirMatter := this.matterDao.CheckWithRootByPath(dirPath, user)
|
||||
|
||||
//如果存在,那么先删除再说。
|
||||
srcMatter := this.matterDao.findByUserUuidAndPath(user.Uuid, subPath)
|
||||
if srcMatter != nil {
|
||||
this.matterService.AtomicDelete(srcMatter)
|
||||
}
|
||||
|
||||
this.matterService.AtomicUpload(request.Body, user, dirMatter, filename, true)
|
||||
|
||||
}
|
||||
|
||||
//删除文件
|
||||
func (this *DavService) HandleDelete(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
|
||||
|
||||
fmt.Printf("DELETE %s\n", subPath)
|
||||
|
||||
//寻找符合条件的matter.
|
||||
matter := this.matterDao.CheckWithRootByPath(subPath, user)
|
||||
|
||||
this.matterService.AtomicDelete(matter)
|
||||
}
|
||||
|
||||
//创建文件夹
|
||||
func (this *DavService) HandleMkcol(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
|
||||
|
||||
fmt.Printf("MKCOL %s\n", subPath)
|
||||
|
||||
thisDirName := tool.GetFilenameOfPath(subPath)
|
||||
dirPath := tool.GetDirOfPath(subPath)
|
||||
|
||||
//寻找符合条件的matter.
|
||||
dirMatter := this.matterDao.CheckWithRootByPath(dirPath, user)
|
||||
|
||||
this.matterService.AtomicCreateDirectory(dirMatter, thisDirName, user)
|
||||
|
||||
}
|
||||
|
||||
//跨域请求的OPTIONS询问
|
||||
func (this *DavService) HandleOptions(w http.ResponseWriter, r *http.Request, user *User, subPath string) {
|
||||
|
||||
fmt.Printf("OPTIONS %s\n", subPath)
|
||||
|
||||
//寻找符合条件的matter.
|
||||
matter := this.matterDao.CheckWithRootByPath(subPath, user)
|
||||
|
||||
allow := "OPTIONS, LOCK, PUT, MKCOL"
|
||||
if matter.Dir {
|
||||
allow = "OPTIONS, LOCK, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND"
|
||||
} else {
|
||||
allow = "OPTIONS, LOCK, GET, HEAD, POST, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND, PUT"
|
||||
}
|
||||
|
||||
w.Header().Set("Allow", allow)
|
||||
// http://www.webdav.org/specs/rfc4918.html#dav.compliance.classes
|
||||
w.Header().Set("DAV", "1, 2")
|
||||
// http://msdn.microsoft.com/en-au/library/cc250217.aspx
|
||||
w.Header().Set("MS-Author-Via", "DAV")
|
||||
|
||||
}
|
||||
|
||||
//移动或者复制的准备工作
|
||||
func (this *DavService) prepareMoveCopy(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
user *User, subPath string) (
|
||||
srcMatter *Matter,
|
||||
destDirMatter *Matter,
|
||||
srcDirPath string,
|
||||
destinationDirPath string,
|
||||
destinationName string,
|
||||
overwrite bool) {
|
||||
|
||||
//解析出目标路径。
|
||||
destinationStr := request.Header.Get("Destination")
|
||||
|
||||
//解析出Overwrite。
|
||||
overwriteStr := request.Header.Get("Overwrite")
|
||||
|
||||
//有前缀的目标path
|
||||
var fullDestinationPath string
|
||||
//去掉前缀的目标path
|
||||
var destinationPath string
|
||||
|
||||
if destinationStr == "" {
|
||||
panic(result.BadRequest("Header Destination必填"))
|
||||
}
|
||||
|
||||
//如果是重命名,那么就不是http开头了。
|
||||
if strings.HasPrefix(destinationStr, WEBDAV_PREFFIX) {
|
||||
fullDestinationPath = destinationStr
|
||||
} else {
|
||||
destinationUrl, err := url.Parse(destinationStr)
|
||||
this.PanicError(err)
|
||||
if destinationUrl.Host != request.Host {
|
||||
panic(result.BadRequest("Destination Host不一致. %s %s != %s", destinationStr, destinationUrl.Host, request.Host))
|
||||
}
|
||||
fullDestinationPath = destinationUrl.Path
|
||||
}
|
||||
|
||||
//去除 路径中的相对路径 比如 /a/b/../ => /a/
|
||||
fullDestinationPath = path.Clean(fullDestinationPath)
|
||||
|
||||
//去除前缀
|
||||
pattern := fmt.Sprintf(`^%s(.*)$`, WEBDAV_PREFFIX)
|
||||
reg := regexp.MustCompile(pattern)
|
||||
strs := reg.FindStringSubmatch(fullDestinationPath)
|
||||
if len(strs) == 2 {
|
||||
destinationPath = strs[1]
|
||||
} else {
|
||||
panic(result.BadRequest("目标前缀必须为:%s", WEBDAV_PREFFIX))
|
||||
}
|
||||
|
||||
destinationName = tool.GetFilenameOfPath(destinationPath)
|
||||
destinationDirPath = tool.GetDirOfPath(destinationPath)
|
||||
srcDirPath = tool.GetDirOfPath(subPath)
|
||||
|
||||
overwrite = false
|
||||
if overwriteStr == "T" {
|
||||
overwrite = true
|
||||
}
|
||||
|
||||
//如果前后一致,那么相当于没有改变
|
||||
if destinationPath == subPath {
|
||||
return
|
||||
}
|
||||
|
||||
//源matter.
|
||||
//寻找符合条件的matter.
|
||||
srcMatter = this.matterDao.CheckWithRootByPath(subPath, user)
|
||||
|
||||
//如果是空或者/就是请求根目录
|
||||
if srcMatter.Uuid == MATTER_ROOT {
|
||||
panic(result.BadRequest("你不能移动根目录!"))
|
||||
}
|
||||
|
||||
//寻找目标文件夹matter
|
||||
destDirMatter = this.matterDao.CheckWithRootByPath(destinationDirPath, user)
|
||||
|
||||
return srcMatter, destDirMatter, srcDirPath, destinationDirPath, destinationName, overwrite
|
||||
|
||||
}
|
||||
|
||||
//移动或者重命名
|
||||
func (this *DavService) HandleMove(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
|
||||
|
||||
fmt.Printf("MOVE %s\n", subPath)
|
||||
|
||||
srcMatter, destDirMatter, srcDirPath, destinationDirPath, destinationName, overwrite := this.prepareMoveCopy(writer, request, user, subPath)
|
||||
//移动到新目录中去。
|
||||
if destinationDirPath == srcDirPath {
|
||||
//文件夹没变化,相当于重命名。
|
||||
this.matterService.AtomicRename(srcMatter, destinationName, user)
|
||||
} else {
|
||||
this.matterService.AtomicMove(srcMatter, destDirMatter, overwrite)
|
||||
}
|
||||
|
||||
this.logger.Info("完成移动 %s => %s", subPath, destDirMatter.Path)
|
||||
}
|
||||
|
||||
//复制文件/文件夹
|
||||
func (this *DavService) HandleCopy(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
|
||||
|
||||
fmt.Printf("COPY %s\n", subPath)
|
||||
|
||||
srcMatter, destDirMatter, _, _, destinationName, overwrite := this.prepareMoveCopy(writer, request, user, subPath)
|
||||
|
||||
//复制到新目录中去。
|
||||
this.matterService.AtomicCopy(srcMatter, destDirMatter, destinationName, overwrite)
|
||||
|
||||
this.logger.Info("完成复制 %s => %s", subPath, destDirMatter.Path)
|
||||
|
||||
}
|
||||
|
||||
//加锁
|
||||
func (this *DavService) HandleLock(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
|
||||
|
||||
panic(result.BadRequest("不支持LOCK方法"))
|
||||
}
|
||||
|
||||
//解锁
|
||||
func (this *DavService) HandleUnlock(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
|
||||
|
||||
panic(result.BadRequest("不支持UNLOCK方法"))
|
||||
}
|
||||
|
||||
//修改文件属性
|
||||
func (this *DavService) HandleProppatch(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
|
||||
|
||||
panic(result.BadRequest("不支持PROPPATCH方法"))
|
||||
}
|
||||
|
||||
//处理所有的请求
|
||||
func (this *DavService) HandleDav(writer http.ResponseWriter, request *http.Request, user *User, subPath string) {
|
||||
|
||||
method := request.Method
|
||||
if method == "OPTIONS" {
|
||||
|
||||
//跨域问询
|
||||
this.HandleOptions(writer, request, user, subPath)
|
||||
|
||||
} else if method == "GET" || method == "HEAD" || method == "POST" {
|
||||
|
||||
//请求文件详情(下载)
|
||||
this.HandleGetHeadPost(writer, request, user, subPath)
|
||||
|
||||
} else if method == "DELETE" {
|
||||
|
||||
//删除文件
|
||||
this.HandleDelete(writer, request, user, subPath)
|
||||
|
||||
} else if method == "PUT" {
|
||||
|
||||
//上传文件
|
||||
this.HandlePut(writer, request, user, subPath)
|
||||
|
||||
} else if method == "MKCOL" {
|
||||
|
||||
//创建文件夹
|
||||
this.HandleMkcol(writer, request, user, subPath)
|
||||
|
||||
} else if method == "COPY" {
|
||||
|
||||
//复制文件/文件夹
|
||||
this.HandleCopy(writer, request, user, subPath)
|
||||
|
||||
} else if method == "MOVE" {
|
||||
|
||||
//移动(重命名)文件/文件夹
|
||||
this.HandleMove(writer, request, user, subPath)
|
||||
|
||||
} else if method == "LOCK" {
|
||||
|
||||
//加锁
|
||||
this.HandleLock(writer, request, user, subPath)
|
||||
|
||||
} else if method == "UNLOCK" {
|
||||
|
||||
//释放锁
|
||||
this.HandleUnlock(writer, request, user, subPath)
|
||||
|
||||
} else if method == "PROPFIND" {
|
||||
|
||||
//列出文件夹或者目录详情
|
||||
this.HandlePropfind(writer, request, user, subPath)
|
||||
|
||||
} else if method == "PROPPATCH" {
|
||||
|
||||
//修改文件属性
|
||||
this.HandleProppatch(writer, request, user, subPath)
|
||||
|
||||
} else {
|
||||
|
||||
panic(result.BadRequest("该方法还不支持。%s", method))
|
||||
|
||||
}
|
||||
|
||||
}
|
364
code/download/download.go
Normal file
364
code/download/download.go
Normal file
@ -0,0 +1,364 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"tank/code/result"
|
||||
"tank/code/tool"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HttpRange specifies the byte range to be sent to the client.
|
||||
type HttpRange struct {
|
||||
start int64
|
||||
length int64
|
||||
}
|
||||
|
||||
func (r HttpRange) contentRange(size int64) string {
|
||||
return fmt.Sprintf("bytes %d-%d/%d", r.start, r.start+r.length-1, size)
|
||||
}
|
||||
|
||||
func (r HttpRange) mimeHeader(contentType string, size int64) textproto.MIMEHeader {
|
||||
return textproto.MIMEHeader{
|
||||
"Content-Range": {r.contentRange(size)},
|
||||
"Content-Type": {contentType},
|
||||
}
|
||||
}
|
||||
|
||||
// CountingWriter counts how many bytes have been written to it.
|
||||
type CountingWriter int64
|
||||
|
||||
func (w *CountingWriter) Write(p []byte) (n int, err error) {
|
||||
*w += CountingWriter(len(p))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
//检查Last-Modified头。返回true: 请求已经完成了。(言下之意,文件没有修改过) 返回false:文件修改过。
|
||||
func CheckLastModified(w http.ResponseWriter, r *http.Request, modifyTime time.Time) bool {
|
||||
if modifyTime.IsZero() {
|
||||
return false
|
||||
}
|
||||
|
||||
// The Date-Modified header truncates sub-second precision, so
|
||||
// use mtime < t+1s instead of mtime <= t to check for unmodified.
|
||||
if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modifyTime.Before(t.Add(1*time.Second)) {
|
||||
h := w.Header()
|
||||
delete(h, "Content-Type")
|
||||
delete(h, "Content-Length")
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return true
|
||||
}
|
||||
w.Header().Set("Last-Modified", modifyTime.UTC().Format(http.TimeFormat))
|
||||
return false
|
||||
}
|
||||
|
||||
// 处理ETag标签
|
||||
// CheckETag implements If-None-Match and If-Range checks.
|
||||
//
|
||||
// The ETag or modtime must have been previously set in the
|
||||
// ResponseWriter's headers. The modtime is only compared at second
|
||||
// granularity and may be the zero value to mean unknown.
|
||||
//
|
||||
// The return value is the effective request "Range" header to use and
|
||||
// whether this request is now considered done.
|
||||
func CheckETag(w http.ResponseWriter, r *http.Request, modtime time.Time) (rangeReq string, done bool) {
|
||||
etag := w.Header().Get("Etag")
|
||||
rangeReq = r.Header.Get("Range")
|
||||
|
||||
// Invalidate the range request if the entity doesn't match the one
|
||||
// the client was expecting.
|
||||
// "If-Range: version" means "ignore the Range: header unless version matches the
|
||||
// current file."
|
||||
// We only support ETag versions.
|
||||
// The caller must have set the ETag on the response already.
|
||||
if ir := r.Header.Get("If-Range"); ir != "" && ir != etag {
|
||||
// The If-Range value is typically the ETag value, but it may also be
|
||||
// the modtime date. See golang.org/issue/8367.
|
||||
timeMatches := false
|
||||
if !modtime.IsZero() {
|
||||
if t, err := http.ParseTime(ir); err == nil && t.Unix() == modtime.Unix() {
|
||||
timeMatches = true
|
||||
}
|
||||
}
|
||||
if !timeMatches {
|
||||
rangeReq = ""
|
||||
}
|
||||
}
|
||||
|
||||
if inm := r.Header.Get("If-None-Match"); inm != "" {
|
||||
// Must know ETag.
|
||||
if etag == "" {
|
||||
return rangeReq, false
|
||||
}
|
||||
|
||||
// (bradfitz): non-GET/HEAD requests require more work:
|
||||
// sending a different status code on matches, and
|
||||
// also can't use weak cache validators (those with a "W/
|
||||
// prefix). But most users of ServeContent will be using
|
||||
// it on GET or HEAD, so only support those for now.
|
||||
if r.Method != "GET" && r.Method != "HEAD" {
|
||||
return rangeReq, false
|
||||
}
|
||||
|
||||
// (bradfitz): deal with comma-separated or multiple-valued
|
||||
// list of If-None-match values. For now just handle the common
|
||||
// case of a single item.
|
||||
if inm == etag || inm == "*" {
|
||||
h := w.Header()
|
||||
delete(h, "Content-Type")
|
||||
delete(h, "Content-Length")
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return "", true
|
||||
}
|
||||
}
|
||||
return rangeReq, false
|
||||
}
|
||||
|
||||
// ParseRange parses a Range header string as per RFC 2616.
|
||||
func ParseRange(s string, size int64) ([]HttpRange, error) {
|
||||
if s == "" {
|
||||
return nil, nil // header not present
|
||||
}
|
||||
const b = "bytes="
|
||||
if !strings.HasPrefix(s, b) {
|
||||
return nil, errors.New("invalid range")
|
||||
}
|
||||
var ranges []HttpRange
|
||||
for _, ra := range strings.Split(s[len(b):], ",") {
|
||||
ra = strings.TrimSpace(ra)
|
||||
if ra == "" {
|
||||
continue
|
||||
}
|
||||
i := strings.Index(ra, "-")
|
||||
if i < 0 {
|
||||
return nil, errors.New("invalid range")
|
||||
}
|
||||
start, end := strings.TrimSpace(ra[:i]), strings.TrimSpace(ra[i+1:])
|
||||
var r HttpRange
|
||||
if start == "" {
|
||||
// If no start is specified, end specifies the
|
||||
// range start relative to the end of the file.
|
||||
i, err := strconv.ParseInt(end, 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid range")
|
||||
}
|
||||
if i > size {
|
||||
i = size
|
||||
}
|
||||
r.start = size - i
|
||||
r.length = size - r.start
|
||||
} else {
|
||||
i, err := strconv.ParseInt(start, 10, 64)
|
||||
if err != nil || i >= size || i < 0 {
|
||||
return nil, errors.New("invalid range")
|
||||
}
|
||||
r.start = i
|
||||
if end == "" {
|
||||
// If no end is specified, range extends to end of the file.
|
||||
r.length = size - r.start
|
||||
} else {
|
||||
i, err := strconv.ParseInt(end, 10, 64)
|
||||
if err != nil || r.start > i {
|
||||
return nil, errors.New("invalid range")
|
||||
}
|
||||
if i >= size {
|
||||
i = size - 1
|
||||
}
|
||||
r.length = i - r.start + 1
|
||||
}
|
||||
}
|
||||
ranges = append(ranges, r)
|
||||
}
|
||||
return ranges, nil
|
||||
}
|
||||
|
||||
// RangesMIMESize returns the number of bytes it takes to encode the
|
||||
// provided ranges as a multipart response.
|
||||
func RangesMIMESize(ranges []HttpRange, contentType string, contentSize int64) (encSize int64) {
|
||||
var w CountingWriter
|
||||
mw := multipart.NewWriter(&w)
|
||||
for _, ra := range ranges {
|
||||
_, e := mw.CreatePart(ra.mimeHeader(contentType, contentSize))
|
||||
|
||||
PanicError(e)
|
||||
|
||||
encSize += ra.length
|
||||
}
|
||||
e := mw.Close()
|
||||
PanicError(e)
|
||||
encSize += int64(w)
|
||||
return
|
||||
}
|
||||
|
||||
func SumRangesSize(ranges []HttpRange) (size int64) {
|
||||
for _, ra := range ranges {
|
||||
size += ra.length
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
func PanicError(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//文件下载。具有进度功能。
|
||||
//下载功能参考:https://github.com/Masterminds/go-fileserver
|
||||
func DownloadFile(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
filePath string,
|
||||
filename string,
|
||||
withContentDisposition bool) {
|
||||
|
||||
diskFile, err := os.Open(filePath)
|
||||
PanicError(err)
|
||||
|
||||
defer func() {
|
||||
e := diskFile.Close()
|
||||
PanicError(e)
|
||||
}()
|
||||
|
||||
|
||||
//根据参数添加content-disposition。该Header会让浏览器自动下载,而不是预览。
|
||||
if withContentDisposition {
|
||||
fileName := url.QueryEscape(filename)
|
||||
writer.Header().Set("content-disposition", "attachment; filename=\""+fileName+"\"")
|
||||
}
|
||||
|
||||
//显示文件大小。
|
||||
fileInfo, err := diskFile.Stat()
|
||||
if err != nil {
|
||||
panic("无法从磁盘中获取文件信息")
|
||||
}
|
||||
|
||||
modifyTime := fileInfo.ModTime()
|
||||
|
||||
if CheckLastModified(writer, request, modifyTime) {
|
||||
return
|
||||
}
|
||||
rangeReq, done := CheckETag(writer, request, modifyTime)
|
||||
if done {
|
||||
return
|
||||
}
|
||||
|
||||
code := http.StatusOK
|
||||
|
||||
// From net/http/sniff.go
|
||||
// The algorithm uses at most sniffLen bytes to make its decision.
|
||||
const sniffLen = 512
|
||||
|
||||
// If Content-Type isn't set, use the file's extension to find it, but
|
||||
// if the Content-Type is unset explicitly, do not sniff the type.
|
||||
ctypes, haveType := writer.Header()["Content-Type"]
|
||||
var ctype string
|
||||
if !haveType {
|
||||
//使用mimeUtil来获取mime
|
||||
ctype = tool.GetFallbackMimeType(filename, "")
|
||||
if ctype == "" {
|
||||
// read a chunk to decide between utf-8 text and binary
|
||||
var buf [sniffLen]byte
|
||||
n, _ := io.ReadFull(diskFile, buf[:])
|
||||
ctype = http.DetectContentType(buf[:n])
|
||||
_, err := diskFile.Seek(0, os.SEEK_SET) // rewind to output whole file
|
||||
if err != nil {
|
||||
panic("无法准确定位文件")
|
||||
}
|
||||
}
|
||||
writer.Header().Set("Content-Type", ctype)
|
||||
} else if len(ctypes) > 0 {
|
||||
ctype = ctypes[0]
|
||||
}
|
||||
|
||||
size := fileInfo.Size()
|
||||
|
||||
// handle Content-Range header.
|
||||
sendSize := size
|
||||
var sendContent io.Reader = diskFile
|
||||
if size >= 0 {
|
||||
ranges, err := ParseRange(rangeReq, size)
|
||||
if err != nil {
|
||||
panic(result.CustomWebResult(result.CODE_WRAPPER_RANGE_NOT_SATISFIABLE, "range header出错"))
|
||||
}
|
||||
if SumRangesSize(ranges) > size {
|
||||
// The total number of bytes in all the ranges
|
||||
// is larger than the size of the file by
|
||||
// itself, so this is probably an attack, or a
|
||||
// dumb client. Ignore the range request.
|
||||
ranges = nil
|
||||
}
|
||||
switch {
|
||||
case len(ranges) == 1:
|
||||
// RFC 2616, Section 14.16:
|
||||
// "When an HTTP message includes the content of a single
|
||||
// range (for example, a response to a request for a
|
||||
// single range, or to a request for a set of ranges
|
||||
// that overlap without any holes), this content is
|
||||
// transmitted with a Content-Range header, and a
|
||||
// Content-Length header showing the number of bytes
|
||||
// actually transferred.
|
||||
// ...
|
||||
// A response to a request for a single range MUST NOT
|
||||
// be sent using the multipart/byteranges media type."
|
||||
ra := ranges[0]
|
||||
if _, err := diskFile.Seek(ra.start, io.SeekStart); err != nil {
|
||||
panic(result.CustomWebResult(result.CODE_WRAPPER_RANGE_NOT_SATISFIABLE, "range header出错"))
|
||||
}
|
||||
sendSize = ra.length
|
||||
code = http.StatusPartialContent
|
||||
writer.Header().Set("Content-Range", ra.contentRange(size))
|
||||
case len(ranges) > 1:
|
||||
sendSize = RangesMIMESize(ranges, ctype, size)
|
||||
code = http.StatusPartialContent
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
mw := multipart.NewWriter(pw)
|
||||
writer.Header().Set("Content-Type", "multipart/byteranges; boundary="+mw.Boundary())
|
||||
sendContent = pr
|
||||
defer pr.Close() // cause writing goroutine to fail and exit if CopyN doesn't finish.
|
||||
go func() {
|
||||
for _, ra := range ranges {
|
||||
part, err := mw.CreatePart(ra.mimeHeader(ctype, size))
|
||||
if err != nil {
|
||||
pw.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
if _, err := diskFile.Seek(ra.start, io.SeekStart); err != nil {
|
||||
pw.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
if _, err := io.CopyN(part, diskFile, ra.length); err != nil {
|
||||
pw.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
mw.Close()
|
||||
pw.Close()
|
||||
}()
|
||||
}
|
||||
|
||||
writer.Header().Set("Accept-Ranges", "bytes")
|
||||
if writer.Header().Get("Content-Encoding") == "" {
|
||||
writer.Header().Set("Content-Length", strconv.FormatInt(sendSize, 10))
|
||||
}
|
||||
}
|
||||
|
||||
writer.WriteHeader(code)
|
||||
|
||||
if request.Method != "HEAD" {
|
||||
io.CopyN(writer, sendContent, sendSize)
|
||||
}
|
||||
|
||||
}
|
67
code/download_token_dao.go
Normal file
67
code/download_token_dao.go
Normal file
@ -0,0 +1,67 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
|
||||
"github.com/nu7hatch/gouuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DownloadTokenDao struct {
|
||||
BaseDao
|
||||
}
|
||||
|
||||
//按照Id查询
|
||||
func (this *DownloadTokenDao) FindByUuid(uuid string) *DownloadToken {
|
||||
|
||||
// Read
|
||||
var downloadToken = &DownloadToken{}
|
||||
db := CONTEXT.DB.Where(&DownloadToken{Base: Base{Uuid: uuid}}).First(downloadToken)
|
||||
if db.Error != nil {
|
||||
return nil
|
||||
}
|
||||
return downloadToken
|
||||
|
||||
}
|
||||
|
||||
//按照Id查询
|
||||
func (this *DownloadTokenDao) CheckByUuid(uuid string) *DownloadToken {
|
||||
|
||||
// Read
|
||||
var downloadToken = &DownloadToken{}
|
||||
db := CONTEXT.DB.Where(&DownloadToken{Base: Base{Uuid: uuid}}).First(downloadToken)
|
||||
this.PanicError(db.Error)
|
||||
return downloadToken
|
||||
|
||||
}
|
||||
|
||||
//创建一个session并且持久化到数据库中。
|
||||
func (this *DownloadTokenDao) Create(downloadToken *DownloadToken) *DownloadToken {
|
||||
|
||||
timeUUID, _ := uuid.NewV4()
|
||||
downloadToken.Uuid = string(timeUUID.String())
|
||||
|
||||
downloadToken.CreateTime = time.Now()
|
||||
downloadToken.UpdateTime = time.Now()
|
||||
downloadToken.Sort = time.Now().UnixNano() / 1e6
|
||||
db := CONTEXT.DB.Create(downloadToken)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return downloadToken
|
||||
}
|
||||
|
||||
//修改一个downloadToken
|
||||
func (this *DownloadTokenDao) Save(downloadToken *DownloadToken) *DownloadToken {
|
||||
|
||||
downloadToken.UpdateTime = time.Now()
|
||||
db := CONTEXT.DB.Save(downloadToken)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return downloadToken
|
||||
}
|
||||
|
||||
//执行清理操作
|
||||
func (this *DownloadTokenDao) Cleanup() {
|
||||
this.logger.Info("[DownloadTokenDao]执行清理:清除数据库中所有DownloadToken记录。")
|
||||
db := CONTEXT.DB.Where("uuid is not null").Delete(DownloadToken{})
|
||||
this.PanicError(db.Error)
|
||||
}
|
18
code/download_token_model.go
Normal file
18
code/download_token_model.go
Normal file
@ -0,0 +1,18 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"tank/code/config"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DownloadToken struct {
|
||||
Base
|
||||
UserUuid string `json:"userUuid" gorm:"type:char(36) not null"`
|
||||
MatterUuid string `json:"matterUuid" gorm:"type:char(36) not null;index:idx_mu"`
|
||||
ExpireTime time.Time `json:"expireTime" gorm:"type:timestamp not null;default:'2018-01-01 00:00:00'"`
|
||||
Ip string `json:"ip" gorm:"type:varchar(128) not null"`
|
||||
}
|
||||
|
||||
func (this *DownloadToken) TableName() string {
|
||||
return config.TABLE_PREFIX + "download_token"
|
||||
}
|
126
code/footprint_controller.go
Normal file
126
code/footprint_controller.go
Normal file
@ -0,0 +1,126 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"tank/code/result"
|
||||
)
|
||||
|
||||
type FootprintController struct {
|
||||
BaseController
|
||||
footprintDao *FootprintDao
|
||||
footprintService *FootprintService
|
||||
}
|
||||
|
||||
//初始化方法
|
||||
func (this *FootprintController) Init() {
|
||||
this.BaseController.Init()
|
||||
|
||||
//手动装填本实例的Bean. 这里必须要用中间变量方可。
|
||||
b := CONTEXT.GetBean(this.footprintDao)
|
||||
if b, ok := b.(*FootprintDao); ok {
|
||||
this.footprintDao = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.footprintService)
|
||||
if b, ok := b.(*FootprintService); ok {
|
||||
this.footprintService = b
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//注册自己的路由。
|
||||
func (this *FootprintController) RegisterRoutes() map[string]func(writer http.ResponseWriter, request *http.Request) {
|
||||
|
||||
routeMap := make(map[string]func(writer http.ResponseWriter, request *http.Request))
|
||||
|
||||
//每个Controller需要主动注册自己的路由。
|
||||
routeMap["/api/footprint/delete"] = this.Wrap(this.Delete, USER_ROLE_USER)
|
||||
routeMap["/api/footprint/detail"] = this.Wrap(this.Detail, USER_ROLE_USER)
|
||||
routeMap["/api/footprint/page"] = this.Wrap(this.Page, USER_ROLE_USER)
|
||||
|
||||
return routeMap
|
||||
}
|
||||
|
||||
//查看详情。
|
||||
func (this *FootprintController) Detail(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
uuid := request.FormValue("uuid")
|
||||
if uuid == "" {
|
||||
panic(result.BadRequest("图片缓存的uuid必填"))
|
||||
}
|
||||
|
||||
footprint := this.footprintService.Detail(uuid)
|
||||
|
||||
//验证当前之人是否有权限查看这么详细。
|
||||
user := this.checkUser(writer, request)
|
||||
if user.Role != USER_ROLE_ADMINISTRATOR {
|
||||
if footprint.UserUuid != user.Uuid {
|
||||
panic("没有权限查看该图片缓存")
|
||||
}
|
||||
}
|
||||
|
||||
return this.Success(footprint)
|
||||
|
||||
}
|
||||
|
||||
//按照分页的方式查询
|
||||
func (this *FootprintController) Page(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
//如果是根目录,那么就传入root.
|
||||
pageStr := request.FormValue("page")
|
||||
pageSizeStr := request.FormValue("pageSize")
|
||||
userUuid := request.FormValue("userUuid")
|
||||
orderCreateTime := request.FormValue("orderCreateTime")
|
||||
orderSize := request.FormValue("orderSize")
|
||||
|
||||
user := this.checkUser(writer, request)
|
||||
if user.Role != USER_ROLE_ADMINISTRATOR {
|
||||
userUuid = user.Uuid
|
||||
}
|
||||
|
||||
var page int
|
||||
if pageStr != "" {
|
||||
page, _ = strconv.Atoi(pageStr)
|
||||
}
|
||||
|
||||
pageSize := 200
|
||||
if pageSizeStr != "" {
|
||||
tmp, err := strconv.Atoi(pageSizeStr)
|
||||
if err == nil {
|
||||
pageSize = tmp
|
||||
}
|
||||
}
|
||||
|
||||
sortArray := []OrderPair{
|
||||
{
|
||||
key: "create_time",
|
||||
value: orderCreateTime,
|
||||
},
|
||||
{
|
||||
key: "size",
|
||||
value: orderSize,
|
||||
},
|
||||
}
|
||||
|
||||
pager := this.footprintDao.Page(page, pageSize, userUuid, sortArray)
|
||||
|
||||
return this.Success(pager)
|
||||
}
|
||||
|
||||
//删除一条记录
|
||||
func (this *FootprintController) Delete(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
uuid := request.FormValue("uuid")
|
||||
if uuid == "" {
|
||||
panic(result.BadRequest("uuid必填"))
|
||||
}
|
||||
|
||||
footprint := this.footprintDao.FindByUuid(uuid)
|
||||
|
||||
if footprint != nil {
|
||||
this.footprintDao.Delete(footprint)
|
||||
}
|
||||
|
||||
return this.Success("删除成功!")
|
||||
}
|
127
code/footprint_dao.go
Normal file
127
code/footprint_dao.go
Normal file
@ -0,0 +1,127 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/nu7hatch/gouuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FootprintDao struct {
|
||||
BaseDao
|
||||
}
|
||||
|
||||
//按照Id查询文件
|
||||
func (this *FootprintDao) FindByUuid(uuid string) *Footprint {
|
||||
|
||||
// Read
|
||||
var footprint Footprint
|
||||
db := CONTEXT.DB.Where(&Footprint{Base: Base{Uuid: uuid}}).First(&footprint)
|
||||
if db.Error != nil {
|
||||
return nil
|
||||
}
|
||||
return &footprint
|
||||
}
|
||||
|
||||
//按照Id查询文件
|
||||
func (this *FootprintDao) CheckByUuid(uuid string) *Footprint {
|
||||
|
||||
// Read
|
||||
var footprint Footprint
|
||||
db := CONTEXT.DB.Where(&Footprint{Base: Base{Uuid: uuid}}).First(&footprint)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return &footprint
|
||||
|
||||
}
|
||||
|
||||
//按分页条件获取分页
|
||||
func (this *FootprintDao) Page(page int, pageSize int, userUuid string, sortArray []OrderPair) *Pager {
|
||||
|
||||
var wp = &WherePair{}
|
||||
|
||||
if userUuid != "" {
|
||||
wp = wp.And(&WherePair{Query: "user_uuid = ?", Args: []interface{}{userUuid}})
|
||||
}
|
||||
|
||||
var conditionDB *gorm.DB
|
||||
conditionDB = CONTEXT.DB.Model(&Footprint{}).Where(wp.Query, wp.Args...)
|
||||
|
||||
count := 0
|
||||
db := conditionDB.Count(&count)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
var footprints []*Footprint
|
||||
db = conditionDB.Order(this.GetSortString(sortArray)).Offset(page * pageSize).Limit(pageSize).Find(&footprints)
|
||||
this.PanicError(db.Error)
|
||||
pager := NewPager(page, pageSize, count, footprints)
|
||||
|
||||
return pager
|
||||
}
|
||||
|
||||
//创建
|
||||
func (this *FootprintDao) Create(footprint *Footprint) *Footprint {
|
||||
|
||||
timeUUID, _ := uuid.NewV4()
|
||||
footprint.Uuid = string(timeUUID.String())
|
||||
footprint.CreateTime = time.Now()
|
||||
footprint.UpdateTime = time.Now()
|
||||
footprint.Sort = time.Now().UnixNano() / 1e6
|
||||
db := CONTEXT.DB.Create(footprint)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return footprint
|
||||
}
|
||||
|
||||
//修改一条记录
|
||||
func (this *FootprintDao) Save(footprint *Footprint) *Footprint {
|
||||
|
||||
footprint.UpdateTime = time.Now()
|
||||
db := CONTEXT.DB.Save(footprint)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return footprint
|
||||
}
|
||||
|
||||
//删除一条记录
|
||||
func (this *FootprintDao) Delete(footprint *Footprint) {
|
||||
|
||||
db := CONTEXT.DB.Delete(&footprint)
|
||||
this.PanicError(db.Error)
|
||||
}
|
||||
|
||||
//获取一段时间中,总的数量
|
||||
func (this *FootprintDao) CountBetweenTime(startTime time.Time, endTime time.Time) int64 {
|
||||
var count int64
|
||||
db := CONTEXT.DB.Model(&Footprint{}).Where("create_time >= ? AND create_time <= ?", startTime, endTime).Count(&count)
|
||||
this.PanicError(db.Error)
|
||||
return count
|
||||
}
|
||||
|
||||
//获取一段时间中UV的数量
|
||||
func (this *FootprintDao) UvBetweenTime(startTime time.Time, endTime time.Time) int64 {
|
||||
var count int64
|
||||
db := CONTEXT.DB.Model(&Footprint{}).Where("create_time >= ? AND create_time <= ?", startTime, endTime).Select("COUNT(DISTINCT(ip))")
|
||||
this.PanicError(db.Error)
|
||||
row := db.Row()
|
||||
row.Scan(&count)
|
||||
return count
|
||||
}
|
||||
|
||||
//获取一段时间中平均耗时
|
||||
func (this *FootprintDao) AvgCostBetweenTime(startTime time.Time, endTime time.Time) int64 {
|
||||
var cost float64
|
||||
db := CONTEXT.DB.Model(&Footprint{}).Where("create_time >= ? AND create_time <= ?", startTime, endTime).Select("AVG(cost)")
|
||||
this.PanicError(db.Error)
|
||||
row := db.Row()
|
||||
row.Scan(&cost)
|
||||
return int64(cost)
|
||||
}
|
||||
|
||||
|
||||
//执行清理操作
|
||||
func (this *FootprintDao) Cleanup() {
|
||||
this.logger.Info("[FootprintDao]执行清理:清除数据库中所有Footprint记录。")
|
||||
db := CONTEXT.DB.Where("uuid is not null").Delete(Footprint{})
|
||||
this.PanicError(db.Error)
|
||||
}
|
22
code/footprint_model.go
Normal file
22
code/footprint_model.go
Normal file
@ -0,0 +1,22 @@
|
||||
package code
|
||||
|
||||
import "tank/code/config"
|
||||
|
||||
/**
|
||||
* 系统的所有访问记录均记录在此
|
||||
*/
|
||||
type Footprint struct {
|
||||
Base
|
||||
UserUuid string `json:"userUuid" gorm:"type:char(36)"`
|
||||
Ip string `json:"ip" gorm:"type:varchar(128) not null;index:idx_dt"`
|
||||
Host string `json:"host" gorm:"type:varchar(45) not null"`
|
||||
Uri string `json:"uri" gorm:"type:varchar(255) not null"`
|
||||
Params string `json:"params" gorm:"type:text"`
|
||||
Cost int64 `json:"cost" gorm:"type:bigint(20) not null;default:0"`
|
||||
Success bool `json:"success" gorm:"type:tinyint(1) not null;default:0"`
|
||||
}
|
||||
|
||||
// set File's table name to be `profiles`
|
||||
func (this *Footprint) TableName() string {
|
||||
return config.TABLE_PREFIX + "footprint"
|
||||
}
|
90
code/footprint_service.go
Normal file
90
code/footprint_service.go
Normal file
@ -0,0 +1,90 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"tank/code/config"
|
||||
"tank/code/tool"
|
||||
"time"
|
||||
)
|
||||
|
||||
//@Service
|
||||
type FootprintService struct {
|
||||
Bean
|
||||
footprintDao *FootprintDao
|
||||
userDao *UserDao
|
||||
}
|
||||
|
||||
//初始化方法
|
||||
func (this *FootprintService) Init() {
|
||||
this.Bean.Init()
|
||||
|
||||
//手动装填本实例的Bean. 这里必须要用中间变量方可。
|
||||
b := CONTEXT.GetBean(this.footprintDao)
|
||||
if b, ok := b.(*FootprintDao); ok {
|
||||
this.footprintDao = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.userDao)
|
||||
if b, ok := b.(*UserDao); ok {
|
||||
this.userDao = b
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//获取某个文件的详情,会把父级依次倒着装进去。如果中途出错,直接抛出异常。
|
||||
func (this *FootprintService) Detail(uuid string) *Footprint {
|
||||
|
||||
footprint := this.footprintDao.CheckByUuid(uuid)
|
||||
|
||||
return footprint
|
||||
}
|
||||
|
||||
//记录访问记录
|
||||
func (this *FootprintService) Trace(writer http.ResponseWriter, request *http.Request, duration time.Duration, success bool) {
|
||||
|
||||
params := make(map[string][]string)
|
||||
|
||||
//POST请求参数
|
||||
values := request.PostForm
|
||||
for key, val := range values {
|
||||
params[key] = val
|
||||
}
|
||||
//GET请求参数
|
||||
values1 := request.URL.Query()
|
||||
for key, val := range values1 {
|
||||
params[key] = val
|
||||
}
|
||||
|
||||
//用json的方式输出返回值。
|
||||
paramsString := "{}"
|
||||
paramsData, err := json.Marshal(params)
|
||||
if err == nil {
|
||||
paramsString = string(paramsData)
|
||||
}
|
||||
|
||||
//将文件信息存入数据库中。
|
||||
footprint := &Footprint{
|
||||
Ip: tool.GetIpAddress(request),
|
||||
Host: request.Host,
|
||||
Uri: request.URL.Path,
|
||||
Params: paramsString,
|
||||
Cost: int64(duration / time.Millisecond),
|
||||
Success: success,
|
||||
}
|
||||
|
||||
//有可能DB尚且没有配置 直接打印出内容,并且退出
|
||||
if config.CONFIG.Installed {
|
||||
user := this.findUser(writer, request)
|
||||
userUuid := ""
|
||||
if user != nil {
|
||||
userUuid = user.Uuid
|
||||
}
|
||||
footprint.UserUuid = userUuid
|
||||
footprint = this.footprintDao.Create(footprint)
|
||||
}
|
||||
|
||||
//用json的方式输出返回值。
|
||||
this.logger.Info("Ip:%s Host:%s Uri:%s Params:%s Cost:%d", footprint.Ip, footprint.Host, footprint.Uri, paramsString, int64(duration/time.Millisecond))
|
||||
|
||||
}
|
172
code/image_cache_controller.go
Normal file
172
code/image_cache_controller.go
Normal file
@ -0,0 +1,172 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"tank/code/result"
|
||||
)
|
||||
|
||||
type ImageCacheController struct {
|
||||
BaseController
|
||||
imageCacheDao *ImageCacheDao
|
||||
imageCacheService *ImageCacheService
|
||||
}
|
||||
|
||||
//初始化方法
|
||||
func (this *ImageCacheController) Init() {
|
||||
this.BaseController.Init()
|
||||
|
||||
//手动装填本实例的Bean. 这里必须要用中间变量方可。
|
||||
b := CONTEXT.GetBean(this.imageCacheDao)
|
||||
if b, ok := b.(*ImageCacheDao); ok {
|
||||
this.imageCacheDao = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.imageCacheService)
|
||||
if b, ok := b.(*ImageCacheService); ok {
|
||||
this.imageCacheService = b
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//注册自己的路由。
|
||||
func (this *ImageCacheController) RegisterRoutes() map[string]func(writer http.ResponseWriter, request *http.Request) {
|
||||
|
||||
routeMap := make(map[string]func(writer http.ResponseWriter, request *http.Request))
|
||||
|
||||
//每个Controller需要主动注册自己的路由。
|
||||
routeMap["/api/image/cache/delete"] = this.Wrap(this.Delete, USER_ROLE_USER)
|
||||
routeMap["/api/image/cache/delete/batch"] = this.Wrap(this.DeleteBatch, USER_ROLE_USER)
|
||||
routeMap["/api/image/cache/detail"] = this.Wrap(this.Detail, USER_ROLE_USER)
|
||||
routeMap["/api/image/cache/page"] = this.Wrap(this.Page, USER_ROLE_USER)
|
||||
|
||||
return routeMap
|
||||
}
|
||||
|
||||
//查看某个图片缓存的详情。
|
||||
func (this *ImageCacheController) Detail(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
uuid := request.FormValue("uuid")
|
||||
if uuid == "" {
|
||||
panic(result.BadRequest("图片缓存的uuid必填"))
|
||||
}
|
||||
|
||||
imageCache := this.imageCacheService.Detail(uuid)
|
||||
|
||||
//验证当前之人是否有权限查看这么详细。
|
||||
user := this.checkUser(writer, request)
|
||||
if user.Role != USER_ROLE_ADMINISTRATOR {
|
||||
if imageCache.UserUuid != user.Uuid {
|
||||
panic("没有权限查看该图片缓存")
|
||||
}
|
||||
}
|
||||
|
||||
return this.Success(imageCache)
|
||||
|
||||
}
|
||||
|
||||
//按照分页的方式获取某个图片缓存夹下图片缓存和子图片缓存夹的列表,通常情况下只有一页。
|
||||
func (this *ImageCacheController) Page(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
//如果是根目录,那么就传入root.
|
||||
pageStr := request.FormValue("page")
|
||||
pageSizeStr := request.FormValue("pageSize")
|
||||
orderCreateTime := request.FormValue("orderCreateTime")
|
||||
orderUpdateTime := request.FormValue("orderUpdateTime")
|
||||
orderSort := request.FormValue("orderSort")
|
||||
|
||||
userUuid := request.FormValue("userUuid")
|
||||
matterUuid := request.FormValue("matterUuid")
|
||||
orderSize := request.FormValue("orderSize")
|
||||
|
||||
user := this.checkUser(writer, request)
|
||||
if user.Role != USER_ROLE_ADMINISTRATOR {
|
||||
userUuid = user.Uuid
|
||||
}
|
||||
|
||||
var page int
|
||||
if pageStr != "" {
|
||||
page, _ = strconv.Atoi(pageStr)
|
||||
}
|
||||
|
||||
pageSize := 200
|
||||
if pageSizeStr != "" {
|
||||
tmp, err := strconv.Atoi(pageSizeStr)
|
||||
if err == nil {
|
||||
pageSize = tmp
|
||||
}
|
||||
}
|
||||
|
||||
sortArray := []OrderPair{
|
||||
{
|
||||
key: "create_time",
|
||||
value: orderCreateTime,
|
||||
},
|
||||
{
|
||||
key: "update_time",
|
||||
value: orderUpdateTime,
|
||||
},
|
||||
{
|
||||
key: "sort",
|
||||
value: orderSort,
|
||||
},
|
||||
|
||||
{
|
||||
key: "size",
|
||||
value: orderSize,
|
||||
},
|
||||
}
|
||||
|
||||
pager := this.imageCacheDao.Page(page, pageSize, userUuid, matterUuid, sortArray)
|
||||
|
||||
return this.Success(pager)
|
||||
}
|
||||
|
||||
//删除一个图片缓存
|
||||
func (this *ImageCacheController) Delete(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
uuid := request.FormValue("uuid")
|
||||
if uuid == "" {
|
||||
panic(result.BadRequest("图片缓存的uuid必填"))
|
||||
}
|
||||
|
||||
imageCache := this.imageCacheDao.FindByUuid(uuid)
|
||||
|
||||
//判断图片缓存的所属人是否正确
|
||||
user := this.checkUser(writer, request)
|
||||
if user.Role != USER_ROLE_ADMINISTRATOR && imageCache.UserUuid != user.Uuid {
|
||||
|
||||
panic(result.Unauthorized("没有权限"))
|
||||
}
|
||||
|
||||
this.imageCacheDao.Delete(imageCache)
|
||||
|
||||
return this.Success("删除成功!")
|
||||
}
|
||||
|
||||
//删除一系列图片缓存。
|
||||
func (this *ImageCacheController) DeleteBatch(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
uuids := request.FormValue("uuids")
|
||||
if uuids == "" {
|
||||
panic(result.BadRequest("图片缓存的uuids必填"))
|
||||
}
|
||||
|
||||
uuidArray := strings.Split(uuids, ",")
|
||||
|
||||
for _, uuid := range uuidArray {
|
||||
|
||||
imageCache := this.imageCacheDao.FindByUuid(uuid)
|
||||
|
||||
//判断图片缓存的所属人是否正确
|
||||
user := this.checkUser(writer, request)
|
||||
if user.Role != USER_ROLE_ADMINISTRATOR && imageCache.UserUuid != user.Uuid {
|
||||
panic(result.Unauthorized("没有权限"))
|
||||
}
|
||||
|
||||
this.imageCacheDao.Delete(imageCache)
|
||||
|
||||
}
|
||||
|
||||
return this.Success("删除成功!")
|
||||
}
|
209
code/image_cache_dao.go
Normal file
209
code/image_cache_dao.go
Normal file
@ -0,0 +1,209 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/nu7hatch/gouuid"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"tank/code/tool"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ImageCacheDao struct {
|
||||
BaseDao
|
||||
}
|
||||
|
||||
//按照Id查询文件
|
||||
func (this *ImageCacheDao) FindByUuid(uuid string) *ImageCache {
|
||||
|
||||
// Read
|
||||
var imageCache ImageCache
|
||||
db := CONTEXT.DB.Where(&ImageCache{Base: Base{Uuid: uuid}}).First(&imageCache)
|
||||
if db.Error != nil {
|
||||
return nil
|
||||
}
|
||||
return &imageCache
|
||||
}
|
||||
|
||||
//按照Id查询文件
|
||||
func (this *ImageCacheDao) CheckByUuid(uuid string) *ImageCache {
|
||||
|
||||
// Read
|
||||
var imageCache ImageCache
|
||||
db := CONTEXT.DB.Where(&ImageCache{Base: Base{Uuid: uuid}}).First(&imageCache)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return &imageCache
|
||||
|
||||
}
|
||||
|
||||
//按照名字查询文件夹
|
||||
func (this *ImageCacheDao) FindByMatterUuidAndMode(matterUuid string, mode string) *ImageCache {
|
||||
|
||||
var wp = &WherePair{}
|
||||
|
||||
if matterUuid != "" {
|
||||
wp = wp.And(&WherePair{Query: "matter_uuid = ?", Args: []interface{}{matterUuid}})
|
||||
}
|
||||
|
||||
if mode != "" {
|
||||
wp = wp.And(&WherePair{Query: "mode = ?", Args: []interface{}{mode}})
|
||||
}
|
||||
|
||||
var imageCache = &ImageCache{}
|
||||
db := CONTEXT.DB.Model(&ImageCache{}).Where(wp.Query, wp.Args...).First(imageCache)
|
||||
|
||||
if db.Error != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return imageCache
|
||||
}
|
||||
|
||||
//按照id和userUuid来查找。找不到抛异常。
|
||||
func (this *ImageCacheDao) CheckByUuidAndUserUuid(uuid string, userUuid string) *ImageCache {
|
||||
|
||||
// Read
|
||||
var imageCache = &ImageCache{}
|
||||
db := CONTEXT.DB.Where(&ImageCache{Base: Base{Uuid: uuid}, UserUuid: userUuid}).First(imageCache)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return imageCache
|
||||
|
||||
}
|
||||
|
||||
//获取某个用户的某个文件夹下的某个名字的文件(或文件夹)列表
|
||||
func (this *ImageCacheDao) ListByUserUuidAndPuuidAndDirAndName(userUuid string) []*ImageCache {
|
||||
|
||||
var imageCaches []*ImageCache
|
||||
|
||||
db := CONTEXT.DB.
|
||||
Where(ImageCache{UserUuid: userUuid}).
|
||||
Find(&imageCaches)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return imageCaches
|
||||
}
|
||||
|
||||
//获取某个文件夹下所有的文件和子文件
|
||||
func (this *ImageCacheDao) Page(page int, pageSize int, userUuid string, matterUuid string, sortArray []OrderPair) *Pager {
|
||||
|
||||
var wp = &WherePair{}
|
||||
|
||||
if userUuid != "" {
|
||||
wp = wp.And(&WherePair{Query: "user_uuid = ?", Args: []interface{}{userUuid}})
|
||||
}
|
||||
|
||||
if matterUuid != "" {
|
||||
wp = wp.And(&WherePair{Query: "matter_uuid = ?", Args: []interface{}{matterUuid}})
|
||||
}
|
||||
|
||||
var conditionDB *gorm.DB
|
||||
conditionDB = CONTEXT.DB.Model(&ImageCache{}).Where(wp.Query, wp.Args...)
|
||||
|
||||
count := 0
|
||||
db := conditionDB.Count(&count)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
var imageCaches []*ImageCache
|
||||
db = conditionDB.Order(this.GetSortString(sortArray)).Offset(page * pageSize).Limit(pageSize).Find(&imageCaches)
|
||||
this.PanicError(db.Error)
|
||||
pager := NewPager(page, pageSize, count, imageCaches)
|
||||
|
||||
return pager
|
||||
}
|
||||
|
||||
//创建
|
||||
func (this *ImageCacheDao) Create(imageCache *ImageCache) *ImageCache {
|
||||
|
||||
timeUUID, _ := uuid.NewV4()
|
||||
imageCache.Uuid = string(timeUUID.String())
|
||||
imageCache.CreateTime = time.Now()
|
||||
imageCache.UpdateTime = time.Now()
|
||||
imageCache.Sort = time.Now().UnixNano() / 1e6
|
||||
db := CONTEXT.DB.Create(imageCache)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return imageCache
|
||||
}
|
||||
|
||||
//修改一个文件
|
||||
func (this *ImageCacheDao) Save(imageCache *ImageCache) *ImageCache {
|
||||
|
||||
imageCache.UpdateTime = time.Now()
|
||||
db := CONTEXT.DB.Save(imageCache)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return imageCache
|
||||
}
|
||||
|
||||
//删除一个文件包括文件夹
|
||||
func (this *ImageCacheDao) deleteFileAndDir(imageCache *ImageCache) {
|
||||
|
||||
filePath := GetUserCacheRootDir(imageCache.Username) + imageCache.Path
|
||||
|
||||
dirPath := filepath.Dir(filePath)
|
||||
|
||||
//删除文件
|
||||
err := os.Remove(filePath)
|
||||
if err != nil {
|
||||
this.logger.Error(fmt.Sprintf("删除磁盘上的文件%s出错 %s", filePath, err.Error()))
|
||||
}
|
||||
|
||||
//如果这一层文件夹是空的,那么删除文件夹本身。
|
||||
tool.DeleteEmptyDirRecursive(dirPath)
|
||||
|
||||
}
|
||||
|
||||
//删除一个文件,数据库中删除,物理磁盘上删除。
|
||||
func (this *ImageCacheDao) Delete(imageCache *ImageCache) {
|
||||
|
||||
db := CONTEXT.DB.Delete(&imageCache)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
this.deleteFileAndDir(imageCache)
|
||||
|
||||
}
|
||||
|
||||
//删除一个matter对应的所有缓存
|
||||
func (this *ImageCacheDao) DeleteByMatterUuid(matterUuid string) {
|
||||
|
||||
var wp = &WherePair{}
|
||||
|
||||
wp = wp.And(&WherePair{Query: "matter_uuid = ?", Args: []interface{}{matterUuid}})
|
||||
|
||||
//查询出即将删除的图片缓存
|
||||
var imageCaches []*ImageCache
|
||||
db := CONTEXT.DB.Where(wp.Query, wp.Args).Find(&imageCaches)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
//删除文件记录
|
||||
db = CONTEXT.DB.Where(wp.Query, wp.Args).Delete(ImageCache{})
|
||||
this.PanicError(db.Error)
|
||||
|
||||
//删除文件实体
|
||||
for _, imageCache := range imageCaches {
|
||||
this.deleteFileAndDir(imageCache)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//获取一段时间中文件总大小
|
||||
func (this *ImageCacheDao) SizeBetweenTime(startTime time.Time, endTime time.Time) int64 {
|
||||
var size int64
|
||||
db := CONTEXT.DB.Model(&ImageCache{}).Where("create_time >= ? AND create_time <= ?", startTime, endTime).Select("SUM(size)")
|
||||
this.PanicError(db.Error)
|
||||
row := db.Row()
|
||||
row.Scan(&size)
|
||||
return size
|
||||
}
|
||||
|
||||
|
||||
//执行清理操作
|
||||
func (this *ImageCacheDao) Cleanup() {
|
||||
this.logger.Info("[ImageCacheDao]执行清理:清除数据库中所有ImageCache记录。")
|
||||
db := CONTEXT.DB.Where("uuid is not null").Delete(ImageCache{})
|
||||
this.PanicError(db.Error)
|
||||
}
|
||||
|
32
code/image_cache_model.go
Normal file
32
code/image_cache_model.go
Normal file
@ -0,0 +1,32 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"tank/code/config"
|
||||
)
|
||||
|
||||
/**
|
||||
* 图片缓存,对于那些处理过的图片,统一管理在这里。
|
||||
*/
|
||||
type ImageCache struct {
|
||||
Base
|
||||
Name string `json:"name" gorm:"type:varchar(255) not null"`
|
||||
UserUuid string `json:"userUuid" gorm:"type:char(36)"`
|
||||
Username string `json:"username" gorm:"type:varchar(45) not null"`
|
||||
MatterUuid string `json:"matterUuid" gorm:"type:char(36);index:idx_mu"`
|
||||
MatterName string `json:"matterName" gorm:"type:varchar(255) not null"`
|
||||
Mode string `json:"mode" gorm:"type:varchar(512)"`
|
||||
Md5 string `json:"md5" gorm:"type:varchar(45)"`
|
||||
Size int64 `json:"size" gorm:"type:bigint(20) not null;default:0"`
|
||||
Path string `json:"path" gorm:"type:varchar(512)"`
|
||||
Matter *Matter `json:"matter" gorm:"-"`
|
||||
}
|
||||
|
||||
// set File's table name to be `profiles`
|
||||
func (this *ImageCache) TableName() string {
|
||||
return config.TABLE_PREFIX + "image_cache"
|
||||
}
|
||||
|
||||
// 获取该ImageCache的绝对路径。path代表的是相对路径。
|
||||
func (this *ImageCache) AbsolutePath() string {
|
||||
return GetUserCacheRootDir(this.Username) + this.Path
|
||||
}
|
244
code/image_cache_service.go
Normal file
244
code/image_cache_service.go
Normal file
@ -0,0 +1,244 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/disintegration/imaging"
|
||||
"image"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"tank/code/tool"
|
||||
)
|
||||
|
||||
//@Service
|
||||
type ImageCacheService struct {
|
||||
Bean
|
||||
imageCacheDao *ImageCacheDao
|
||||
userDao *UserDao
|
||||
matterDao *MatterDao
|
||||
}
|
||||
|
||||
//初始化方法
|
||||
func (this *ImageCacheService) Init() {
|
||||
this.Bean.Init()
|
||||
|
||||
//手动装填本实例的Bean. 这里必须要用中间变量方可。
|
||||
b := CONTEXT.GetBean(this.imageCacheDao)
|
||||
if b, ok := b.(*ImageCacheDao); ok {
|
||||
this.imageCacheDao = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.userDao)
|
||||
if b, ok := b.(*UserDao); ok {
|
||||
this.userDao = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.matterDao)
|
||||
if b, ok := b.(*MatterDao); ok {
|
||||
this.matterDao = b
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//获取某个文件的详情,会把父级依次倒着装进去。如果中途出错,直接抛出异常。
|
||||
func (this *ImageCacheService) Detail(uuid string) *ImageCache {
|
||||
|
||||
imageCache := this.imageCacheDao.CheckByUuid(uuid)
|
||||
|
||||
return imageCache
|
||||
}
|
||||
|
||||
//获取预处理时必要的参数
|
||||
func (this *ImageCacheService) ResizeParams(request *http.Request) (needProcess bool, resizeMode string, resizeWidth int, resizeHeight int) {
|
||||
var err error
|
||||
|
||||
//1.0 模式准备逐步废弃掉
|
||||
if request.FormValue("imageProcess") == "resize" {
|
||||
//老模式使用 imageResizeM,imageResizeW,imageResizeH
|
||||
imageResizeM := request.FormValue("imageResizeM")
|
||||
if imageResizeM == "" {
|
||||
imageResizeM = "fit"
|
||||
} else if imageResizeM != "fit" && imageResizeM != "fill" && imageResizeM != "fixed" {
|
||||
panic("imageResizeM参数错误")
|
||||
}
|
||||
imageResizeWStr := request.FormValue("imageResizeW")
|
||||
var imageResizeW int
|
||||
if imageResizeWStr != "" {
|
||||
imageResizeW, err = strconv.Atoi(imageResizeWStr)
|
||||
this.PanicError(err)
|
||||
if imageResizeW < 1 || imageResizeW > 4096 {
|
||||
panic("缩放尺寸不能超过4096")
|
||||
}
|
||||
}
|
||||
imageResizeHStr := request.FormValue("imageResizeH")
|
||||
var imageResizeH int
|
||||
if imageResizeHStr != "" {
|
||||
imageResizeH, err = strconv.Atoi(imageResizeHStr)
|
||||
this.PanicError(err)
|
||||
if imageResizeH < 1 || imageResizeH > 4096 {
|
||||
panic("缩放尺寸不能超过4096")
|
||||
}
|
||||
}
|
||||
|
||||
return true, imageResizeM, imageResizeW, imageResizeH
|
||||
} else if request.FormValue("ir") != "" {
|
||||
//新模式使用 mode_w_h 如果w或者h为0表示这项值不设置
|
||||
imageResizeStr := request.FormValue("ir")
|
||||
arr := strings.Split(imageResizeStr, "_")
|
||||
if len(arr) != 3 {
|
||||
panic("参数不符合规范,格式要求为mode_w_h")
|
||||
}
|
||||
|
||||
imageResizeM := arr[0]
|
||||
if imageResizeM == "" {
|
||||
imageResizeM = "fit"
|
||||
} else if imageResizeM != "fit" && imageResizeM != "fill" && imageResizeM != "fixed" {
|
||||
panic("imageResizeM参数错误")
|
||||
}
|
||||
imageResizeWStr := arr[1]
|
||||
var imageResizeW int
|
||||
if imageResizeWStr != "" {
|
||||
imageResizeW, err = strconv.Atoi(imageResizeWStr)
|
||||
this.PanicError(err)
|
||||
if imageResizeW < 0 || imageResizeW > 4096 {
|
||||
panic("缩放尺寸不能超过4096")
|
||||
}
|
||||
}
|
||||
imageResizeHStr := arr[2]
|
||||
var imageResizeH int
|
||||
if imageResizeHStr != "" {
|
||||
imageResizeH, err = strconv.Atoi(imageResizeHStr)
|
||||
this.PanicError(err)
|
||||
if imageResizeH < 0 || imageResizeH > 4096 {
|
||||
panic("缩放尺寸不能超过4096")
|
||||
}
|
||||
}
|
||||
return true, imageResizeM, imageResizeW, imageResizeH
|
||||
} else {
|
||||
return false, "", 0, 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//图片预处理功能。
|
||||
func (this *ImageCacheService) ResizeImage(request *http.Request, filePath string) *image.NRGBA {
|
||||
|
||||
diskFile, err := os.Open(filePath)
|
||||
this.PanicError(err)
|
||||
defer func() {
|
||||
e := diskFile.Close()
|
||||
this.PanicError(e)
|
||||
}()
|
||||
|
||||
_, imageResizeM, imageResizeW, imageResizeH := this.ResizeParams(request)
|
||||
|
||||
//单边缩略
|
||||
if imageResizeM == "fit" {
|
||||
//将图缩略成宽度为100,高度按比例处理。
|
||||
if imageResizeW != 0 {
|
||||
src, err := imaging.Decode(diskFile)
|
||||
this.PanicError(err)
|
||||
return imaging.Resize(src, imageResizeW, 0, imaging.Lanczos)
|
||||
|
||||
} else if imageResizeH != 0 {
|
||||
//将图缩略成高度为100,宽度按比例处理。
|
||||
src, err := imaging.Decode(diskFile)
|
||||
this.PanicError(err)
|
||||
return imaging.Resize(src, 0, imageResizeH, imaging.Lanczos)
|
||||
|
||||
} else {
|
||||
panic("单边缩略必须指定宽或者高")
|
||||
}
|
||||
} else if imageResizeM == "fill" {
|
||||
//固定宽高,自动裁剪
|
||||
if imageResizeW > 0 && imageResizeH > 0 {
|
||||
src, err := imaging.Decode(diskFile)
|
||||
this.PanicError(err)
|
||||
return imaging.Fill(src, imageResizeW, imageResizeH, imaging.Center, imaging.Lanczos)
|
||||
|
||||
} else {
|
||||
panic("固定宽高,自动裁剪 必须同时指定宽和高")
|
||||
}
|
||||
} else if imageResizeM == "fixed" {
|
||||
//强制宽高缩略
|
||||
if imageResizeW > 0 && imageResizeH > 0 {
|
||||
src, err := imaging.Decode(diskFile)
|
||||
this.PanicError(err)
|
||||
return imaging.Resize(src, imageResizeW, imageResizeH, imaging.Lanczos)
|
||||
|
||||
} else {
|
||||
panic("强制宽高缩略必须同时指定宽和高")
|
||||
}
|
||||
} else {
|
||||
panic("不支持" + imageResizeM + "处理模式")
|
||||
}
|
||||
}
|
||||
|
||||
//缓存一张图片
|
||||
func (this *ImageCacheService) cacheImage(writer http.ResponseWriter, request *http.Request, matter *Matter) *ImageCache {
|
||||
|
||||
//当前的文件是否是图片,只有图片才能处理。
|
||||
extension := tool.GetExtension(matter.Name)
|
||||
formats := map[string]imaging.Format{
|
||||
".jpg": imaging.JPEG,
|
||||
".jpeg": imaging.JPEG,
|
||||
".png": imaging.PNG,
|
||||
".tif": imaging.TIFF,
|
||||
".tiff": imaging.TIFF,
|
||||
".bmp": imaging.BMP,
|
||||
".gif": imaging.GIF,
|
||||
}
|
||||
|
||||
_, imageResizeM, imageResizeW, imageResizeH := this.ResizeParams(request)
|
||||
mode := fmt.Sprintf("%s_%d_%d", imageResizeM, imageResizeW, imageResizeH)
|
||||
|
||||
format, ok := formats[extension]
|
||||
if !ok {
|
||||
panic("该图片格式不支持处理")
|
||||
}
|
||||
|
||||
user := this.userDao.FindByUuid(matter.UserUuid)
|
||||
|
||||
//resize图片
|
||||
dstImage := this.ResizeImage(request, matter.AbsolutePath())
|
||||
|
||||
cacheImageName := tool.GetSimpleFileName(matter.Name) + "_" + mode + extension
|
||||
cacheImageRelativePath := tool.GetSimpleFileName(matter.Path) + "_" + mode + extension
|
||||
cacheImageAbsolutePath := GetUserCacheRootDir(user.Username) + tool.GetSimpleFileName(matter.Path) + "_" + mode + extension
|
||||
|
||||
//创建目录。
|
||||
dir := filepath.Dir(cacheImageAbsolutePath)
|
||||
tool.MakeDirAll(dir)
|
||||
|
||||
fileWriter, err := os.Create(cacheImageAbsolutePath)
|
||||
this.PanicError(err)
|
||||
defer func() {
|
||||
e := fileWriter.Close()
|
||||
this.PanicError(e)
|
||||
}()
|
||||
|
||||
//处理后的图片存放在本地
|
||||
err = imaging.Encode(fileWriter, dstImage, format)
|
||||
this.PanicError(err)
|
||||
|
||||
//获取新文件的大小
|
||||
fileInfo, err := fileWriter.Stat()
|
||||
this.PanicError(err)
|
||||
|
||||
//相关信息写到缓存中去
|
||||
imageCache := &ImageCache{
|
||||
Name: cacheImageName,
|
||||
UserUuid: matter.UserUuid,
|
||||
Username: user.Username,
|
||||
MatterUuid: matter.Uuid,
|
||||
MatterName: matter.Name,
|
||||
Mode: mode,
|
||||
Size: fileInfo.Size(),
|
||||
Path: cacheImageRelativePath,
|
||||
}
|
||||
this.imageCacheDao.Create(imageCache)
|
||||
|
||||
return imageCache
|
||||
}
|
465
code/install_controller.go
Normal file
465
code/install_controller.go
Normal file
@ -0,0 +1,465 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/json-iterator/go"
|
||||
"github.com/nu7hatch/gouuid"
|
||||
"go/build"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"tank/code/config"
|
||||
"tank/code/result"
|
||||
"tank/code/tool"
|
||||
"time"
|
||||
)
|
||||
|
||||
//安装程序的接口,只有安装阶段可以访问。
|
||||
type InstallController struct {
|
||||
BaseController
|
||||
uploadTokenDao *UploadTokenDao
|
||||
downloadTokenDao *DownloadTokenDao
|
||||
matterDao *MatterDao
|
||||
matterService *MatterService
|
||||
imageCacheDao *ImageCacheDao
|
||||
imageCacheService *ImageCacheService
|
||||
}
|
||||
|
||||
//初始化方法
|
||||
func (this *InstallController) Init() {
|
||||
this.BaseController.Init()
|
||||
|
||||
//手动装填本实例的Bean.
|
||||
b := CONTEXT.GetBean(this.uploadTokenDao)
|
||||
if c, ok := b.(*UploadTokenDao); ok {
|
||||
this.uploadTokenDao = c
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.downloadTokenDao)
|
||||
if c, ok := b.(*DownloadTokenDao); ok {
|
||||
this.downloadTokenDao = c
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.matterDao)
|
||||
if c, ok := b.(*MatterDao); ok {
|
||||
this.matterDao = c
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.matterService)
|
||||
if c, ok := b.(*MatterService); ok {
|
||||
this.matterService = c
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.imageCacheDao)
|
||||
if c, ok := b.(*ImageCacheDao); ok {
|
||||
this.imageCacheDao = c
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.imageCacheService)
|
||||
if c, ok := b.(*ImageCacheService); ok {
|
||||
this.imageCacheService = c
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//注册自己的路由。
|
||||
func (this *InstallController) RegisterRoutes() map[string]func(writer http.ResponseWriter, request *http.Request) {
|
||||
|
||||
routeMap := make(map[string]func(writer http.ResponseWriter, request *http.Request))
|
||||
|
||||
//每个Controller需要主动注册自己的路由。
|
||||
routeMap["/api/install/verify"] = this.Wrap(this.Verify, USER_ROLE_GUEST)
|
||||
routeMap["/api/install/table/info/list"] = this.Wrap(this.TableInfoList, USER_ROLE_GUEST)
|
||||
routeMap["/api/install/create/table"] = this.Wrap(this.CreateTable, USER_ROLE_GUEST)
|
||||
routeMap["/api/install/admin/list"] = this.Wrap(this.AdminList, USER_ROLE_GUEST)
|
||||
routeMap["/api/install/create/admin"] = this.Wrap(this.CreateAdmin, USER_ROLE_GUEST)
|
||||
routeMap["/api/install/validate/admin"] = this.Wrap(this.ValidateAdmin, USER_ROLE_GUEST)
|
||||
routeMap["/api/install/finish"] = this.Wrap(this.Finish, USER_ROLE_GUEST)
|
||||
|
||||
return routeMap
|
||||
}
|
||||
|
||||
//获取数据库连接
|
||||
func (this *InstallController) openDbConnection(writer http.ResponseWriter, request *http.Request) *gorm.DB {
|
||||
mysqlPortStr := request.FormValue("mysqlPort")
|
||||
mysqlHost := request.FormValue("mysqlHost")
|
||||
mysqlSchema := request.FormValue("mysqlSchema")
|
||||
mysqlUsername := request.FormValue("mysqlUsername")
|
||||
mysqlPassword := request.FormValue("mysqlPassword")
|
||||
|
||||
var mysqlPort int
|
||||
if mysqlPortStr != "" {
|
||||
tmp, err := strconv.Atoi(mysqlPortStr)
|
||||
this.PanicError(err)
|
||||
mysqlPort = tmp
|
||||
}
|
||||
|
||||
mysqlUrl := tool.GetMysqlUrl(mysqlPort, mysqlHost, mysqlSchema, mysqlUsername, mysqlPassword)
|
||||
|
||||
this.logger.Info("连接MySQL %s", mysqlUrl)
|
||||
|
||||
var err error = nil
|
||||
db, err := gorm.Open("mysql", mysqlUrl)
|
||||
this.PanicError(err)
|
||||
|
||||
db.LogMode(false)
|
||||
|
||||
return db
|
||||
|
||||
}
|
||||
|
||||
//关闭数据库连接
|
||||
func (this *InstallController) closeDbConnection(db *gorm.DB) {
|
||||
|
||||
if db != nil {
|
||||
err := db.Close()
|
||||
if err != nil {
|
||||
this.logger.Error("关闭数据库连接出错 %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//根据表名获取建表SQL语句
|
||||
func (this *InstallController) getCreateSQLFromFile(tableName string) string {
|
||||
|
||||
//1. 从当前安装目录db下去寻找建表文件。
|
||||
homePath := tool.GetHomePath()
|
||||
filePath := homePath + "/db/" + tableName + ".sql"
|
||||
exists, err := tool.PathExists(filePath)
|
||||
if err != nil {
|
||||
panic(result.Server("从安装目录判断建表语句文件是否存在时出错!"))
|
||||
}
|
||||
|
||||
//2. 从GOPATH下面去找,因为可能是开发环境
|
||||
if !exists {
|
||||
|
||||
this.logger.Info("GOPATH = %s", build.Default.GOPATH)
|
||||
|
||||
filePath1 := filePath
|
||||
filePath = build.Default.GOPATH + "/src/tank/build/db/" + tableName + ".sql"
|
||||
exists, err = tool.PathExists(filePath)
|
||||
if err != nil {
|
||||
panic(result.Server("从GOPATH判断建表语句文件是否存在时出错!"))
|
||||
}
|
||||
|
||||
if !exists {
|
||||
panic(result.Server("%s 或 %s 均不存在,请检查你的安装情况。", filePath1, filePath))
|
||||
}
|
||||
}
|
||||
|
||||
//读取文件内容.
|
||||
bytes, err := ioutil.ReadFile(filePath)
|
||||
this.PanicError(err)
|
||||
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
//根据表名获取建表SQL语句
|
||||
func (this *InstallController) getTableMeta(gormDb *gorm.DB, entity IBase) (bool, []*gorm.StructField, []*gorm.StructField) {
|
||||
|
||||
//挣扎一下,尝试获取建表语句。
|
||||
db := gormDb.Unscoped()
|
||||
scope := db.NewScope(entity)
|
||||
|
||||
tableName := scope.TableName()
|
||||
modelStruct := scope.GetModelStruct()
|
||||
allFields := modelStruct.StructFields
|
||||
var missingFields = make([]*gorm.StructField, 0)
|
||||
|
||||
if !scope.Dialect().HasTable(tableName) {
|
||||
missingFields = append(missingFields, allFields...)
|
||||
|
||||
return false, allFields, missingFields
|
||||
} else {
|
||||
|
||||
for _, field := range allFields {
|
||||
if !scope.Dialect().HasColumn(tableName, field.DBName) {
|
||||
if field.IsNormal {
|
||||
missingFields = append(missingFields, field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true, allFields, missingFields
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//根据表名获取建表SQL语句
|
||||
func (this *InstallController) getTableMetaList(db *gorm.DB) []*InstallTableInfo {
|
||||
|
||||
var tableNames = []IBase{&Dashboard{}, &DownloadToken{}, &Footprint{}, &ImageCache{}, &Matter{}, &Preference{}, &Session{}, &UploadToken{}, &User{}}
|
||||
var installTableInfos []*InstallTableInfo
|
||||
|
||||
for _, iBase := range tableNames {
|
||||
exist, allFields, missingFields := this.getTableMeta(db, iBase)
|
||||
installTableInfos = append(installTableInfos, &InstallTableInfo{
|
||||
Name: iBase.TableName(),
|
||||
TableExist: exist,
|
||||
AllFields: allFields,
|
||||
MissingFields: missingFields,
|
||||
})
|
||||
}
|
||||
|
||||
return installTableInfos
|
||||
}
|
||||
|
||||
//验证表结构是否完整。会直接抛出异常
|
||||
func (this *InstallController) validateTableMetaList(tableInfoList []*InstallTableInfo) {
|
||||
|
||||
for _, tableInfo := range tableInfoList {
|
||||
if tableInfo.TableExist {
|
||||
if len(tableInfo.MissingFields) != 0 {
|
||||
|
||||
var strs []string
|
||||
for _, v := range tableInfo.MissingFields {
|
||||
strs = append(strs, v.DBName)
|
||||
}
|
||||
|
||||
panic(result.BadRequest(fmt.Sprintf("%s 表的以下字段缺失:%v", tableInfo.Name, strs)))
|
||||
}
|
||||
} else {
|
||||
panic(result.BadRequest(tableInfo.Name + "表不存在"))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//验证数据库连接
|
||||
func (this *InstallController) Verify(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
db := this.openDbConnection(writer, request)
|
||||
defer this.closeDbConnection(db)
|
||||
|
||||
this.logger.Info("Ping一下数据库")
|
||||
err := db.DB().Ping()
|
||||
this.PanicError(err)
|
||||
|
||||
return this.Success("OK")
|
||||
}
|
||||
|
||||
//获取需要安装的数据库表
|
||||
func (this *InstallController) TableInfoList(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
db := this.openDbConnection(writer, request)
|
||||
defer this.closeDbConnection(db)
|
||||
|
||||
return this.Success(this.getTableMetaList(db))
|
||||
}
|
||||
|
||||
//创建缺失数据库和表
|
||||
func (this *InstallController) CreateTable(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
var tableNames = []IBase{&Dashboard{}, &DownloadToken{}, &Footprint{}, &ImageCache{}, &Matter{}, &Preference{}, &Session{}, &UploadToken{}, &User{}}
|
||||
var installTableInfos []*InstallTableInfo
|
||||
|
||||
db := this.openDbConnection(writer, request)
|
||||
defer this.closeDbConnection(db)
|
||||
|
||||
for _, iBase := range tableNames {
|
||||
|
||||
//补全缺失字段或者创建数据库表
|
||||
db1 := db.AutoMigrate(iBase)
|
||||
this.PanicError(db1.Error)
|
||||
|
||||
exist, allFields, missingFields := this.getTableMeta(db, iBase)
|
||||
installTableInfos = append(installTableInfos, &InstallTableInfo{
|
||||
Name: iBase.TableName(),
|
||||
TableExist: exist,
|
||||
AllFields: allFields,
|
||||
MissingFields: missingFields,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return this.Success(installTableInfos)
|
||||
|
||||
}
|
||||
|
||||
//获取管理员列表(10条记录)
|
||||
func (this *InstallController) AdminList(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
db := this.openDbConnection(writer, request)
|
||||
defer this.closeDbConnection(db)
|
||||
|
||||
var wp = &WherePair{}
|
||||
|
||||
wp = wp.And(&WherePair{Query: "role = ?", Args: []interface{}{USER_ROLE_ADMINISTRATOR}})
|
||||
|
||||
var users []*User
|
||||
db = db.Where(wp.Query, wp.Args...).Offset(0).Limit(10).Find(&users)
|
||||
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return this.Success(users)
|
||||
}
|
||||
|
||||
//创建管理员
|
||||
func (this *InstallController) CreateAdmin(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
db := this.openDbConnection(writer, request)
|
||||
defer this.closeDbConnection(db)
|
||||
|
||||
adminUsername := request.FormValue("adminUsername")
|
||||
adminEmail := request.FormValue("adminEmail")
|
||||
adminPassword := request.FormValue("adminPassword")
|
||||
|
||||
//验证超级管理员的信息
|
||||
if m, _ := regexp.MatchString(`^[0-9a-zA-Z_]+$`, adminUsername); !m {
|
||||
panic(result.BadRequest(`超级管理员用户名必填,且只能包含字母,数字和'_''`))
|
||||
}
|
||||
|
||||
if len(adminPassword) < 6 {
|
||||
panic(result.BadRequest(`超级管理员密码长度至少为6位`))
|
||||
}
|
||||
|
||||
if adminEmail == "" {
|
||||
panic(result.BadRequest(`超级管理员邮箱必填`))
|
||||
}
|
||||
|
||||
//检查是否有重复。
|
||||
var count1 int64
|
||||
db1 := db.Model(&User{}).Where("email = ?", adminEmail).Count(&count1)
|
||||
this.PanicError(db1.Error)
|
||||
if count1 > 0 {
|
||||
panic(result.BadRequest(`该邮箱已存在`))
|
||||
}
|
||||
|
||||
var count2 int64
|
||||
db2 := db.Model(&User{}).Where("username = ?", adminUsername).Count(&count2)
|
||||
this.PanicError(db2.Error)
|
||||
if count2 > 0 {
|
||||
panic(result.BadRequest(`该用户名已存在`))
|
||||
}
|
||||
|
||||
user := &User{}
|
||||
timeUUID, _ := uuid.NewV4()
|
||||
user.Uuid = string(timeUUID.String())
|
||||
user.CreateTime = time.Now()
|
||||
user.UpdateTime = time.Now()
|
||||
user.LastTime = time.Now()
|
||||
user.Sort = time.Now().UnixNano() / 1e6
|
||||
user.Role = USER_ROLE_ADMINISTRATOR
|
||||
user.Username = adminUsername
|
||||
user.Password = tool.GetBcrypt(adminPassword)
|
||||
user.Email = adminEmail
|
||||
user.Phone = ""
|
||||
user.Gender = USER_GENDER_UNKNOWN
|
||||
user.SizeLimit = -1
|
||||
user.Status = USER_STATUS_OK
|
||||
|
||||
db3 := db.Create(user)
|
||||
this.PanicError(db3.Error)
|
||||
|
||||
return this.Success("OK")
|
||||
|
||||
}
|
||||
|
||||
//(如果数据库中本身存在管理员了)验证管理员
|
||||
func (this *InstallController) ValidateAdmin(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
db := this.openDbConnection(writer, request)
|
||||
defer this.closeDbConnection(db)
|
||||
|
||||
adminEmail := request.FormValue("adminEmail")
|
||||
adminPassword := request.FormValue("adminPassword")
|
||||
|
||||
//验证超级管理员的信息
|
||||
if adminEmail == "" {
|
||||
panic(result.BadRequest(`超级管理员邮箱必填`))
|
||||
}
|
||||
if len(adminPassword) < 6 {
|
||||
panic(result.BadRequest(`超级管理员密码长度至少为6位`))
|
||||
}
|
||||
|
||||
var existEmailUser = &User{}
|
||||
db = db.Where(&User{Email: adminEmail}).First(existEmailUser)
|
||||
if db.Error != nil {
|
||||
panic(result.BadRequest(fmt.Sprintf("%s对应的用户不存在", adminEmail)))
|
||||
}
|
||||
|
||||
if !tool.MatchBcrypt(adminPassword, existEmailUser.Password) {
|
||||
panic(result.BadRequest("邮箱或密码错误"))
|
||||
}
|
||||
|
||||
if existEmailUser.Role != USER_ROLE_ADMINISTRATOR {
|
||||
panic(result.BadRequest("该账号不是管理员"))
|
||||
}
|
||||
|
||||
return this.Success("OK")
|
||||
|
||||
}
|
||||
|
||||
//完成系统安装
|
||||
func (this *InstallController) Finish(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
mysqlPortStr := request.FormValue("mysqlPort")
|
||||
mysqlHost := request.FormValue("mysqlHost")
|
||||
mysqlSchema := request.FormValue("mysqlSchema")
|
||||
mysqlUsername := request.FormValue("mysqlUsername")
|
||||
mysqlPassword := request.FormValue("mysqlPassword")
|
||||
|
||||
var mysqlPort int
|
||||
if mysqlPortStr != "" {
|
||||
tmp, err := strconv.Atoi(mysqlPortStr)
|
||||
this.PanicError(err)
|
||||
mysqlPort = tmp
|
||||
}
|
||||
|
||||
//要求数据库连接通畅
|
||||
db := this.openDbConnection(writer, request)
|
||||
defer this.closeDbConnection(db)
|
||||
|
||||
//要求数据库完整。
|
||||
tableMetaList := this.getTableMetaList(db)
|
||||
this.validateTableMetaList(tableMetaList)
|
||||
|
||||
//要求至少有一名管理员。
|
||||
var count1 int64
|
||||
db1 := db.Model(&User{}).Where("role = ?", USER_ROLE_ADMINISTRATOR).Count(&count1)
|
||||
this.PanicError(db1.Error)
|
||||
if count1 == 0 {
|
||||
panic(result.BadRequest(`请至少配置一名管理员`))
|
||||
}
|
||||
|
||||
var configItem = &config.ConfigItem{
|
||||
//默认监听端口号
|
||||
ServerPort: config.CONFIG.ServerPort,
|
||||
//上传的文件路径,要求不以/结尾。如果没有指定,默认在根目录下的matter文件夹中。eg: /var/www/matter
|
||||
MatterPath: config.CONFIG.MatterPath,
|
||||
//mysql相关配置。
|
||||
//数据库端口
|
||||
MysqlPort: mysqlPort,
|
||||
//数据库Host
|
||||
MysqlHost: mysqlHost,
|
||||
//数据库名字
|
||||
MysqlSchema: mysqlSchema,
|
||||
//用户名
|
||||
MysqlUsername: mysqlUsername,
|
||||
//密码
|
||||
MysqlPassword: mysqlPassword,
|
||||
}
|
||||
|
||||
//用json的方式输出返回值。为了让格式更好看。
|
||||
jsonStr, _ := jsoniter.ConfigCompatibleWithStandardLibrary.MarshalIndent(configItem, "", " ")
|
||||
|
||||
//写入到配置文件中(不能使用os.O_APPEND 否则会追加)
|
||||
filePath := tool.GetConfPath() + "/tank.json"
|
||||
f, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0777)
|
||||
this.PanicError(err)
|
||||
_, err = f.Write(jsonStr)
|
||||
this.PanicError(err)
|
||||
err = f.Close()
|
||||
this.PanicError(err)
|
||||
|
||||
//通知配置文件安装完毕。
|
||||
config.CONFIG.InstallOk()
|
||||
|
||||
//通知全局上下文,说系统安装好了
|
||||
CONTEXT.InstallOk()
|
||||
|
||||
return this.Success("OK")
|
||||
}
|
13
code/install_model.go
Normal file
13
code/install_model.go
Normal file
@ -0,0 +1,13 @@
|
||||
package code
|
||||
|
||||
import "github.com/jinzhu/gorm"
|
||||
|
||||
/**
|
||||
* 表名对应的表结构
|
||||
*/
|
||||
type InstallTableInfo struct {
|
||||
Name string `json:"name"`
|
||||
TableExist bool `json:"tableExist"`
|
||||
AllFields []*gorm.StructField `json:"allFields"`
|
||||
MissingFields []*gorm.StructField `json:"missingFields"`
|
||||
}
|
161
code/logger/logger.go
Normal file
161
code/logger/logger.go
Normal file
@ -0,0 +1,161 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"tank/code/tool"
|
||||
"time"
|
||||
)
|
||||
|
||||
//日志系统必须高保
|
||||
//全局唯一的日志对象(在main函数中初始化)
|
||||
var LOGGER = &Logger{}
|
||||
|
||||
//在Logger的基础上包装一个全新的Logger.
|
||||
type Logger struct {
|
||||
//加锁,在维护日志期间,禁止写入日志。
|
||||
sync.RWMutex
|
||||
|
||||
//继承logger
|
||||
goLogger *log.Logger
|
||||
//日志记录所在的文件
|
||||
file *os.File
|
||||
//每天凌晨定时整理器
|
||||
maintainTimer *time.Timer
|
||||
}
|
||||
|
||||
//处理日志的统一方法。
|
||||
func (this *Logger) log(prefix string, format string, v ...interface{}) {
|
||||
|
||||
content := fmt.Sprintf(format+"\r\n", v...)
|
||||
|
||||
//控制台中打印日志,记录行号。
|
||||
_, file, line, ok := runtime.Caller(2)
|
||||
if !ok {
|
||||
file = "???"
|
||||
line = 0
|
||||
}
|
||||
|
||||
var consoleFormat = fmt.Sprintf("%s%s %s:%d %s", prefix, tool.ConvertTimeToTimeString(time.Now()), tool.GetFilenameOfPath(file), line, content)
|
||||
fmt.Printf(consoleFormat)
|
||||
|
||||
this.goLogger.SetPrefix(prefix)
|
||||
|
||||
//每一行我们加上换行符
|
||||
err := this.goLogger.Output(3, content)
|
||||
if err != nil {
|
||||
fmt.Printf("occur error while logging %s \r\n", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
//处理日志的统一方法。
|
||||
func (this *Logger) Debug(format string, v ...interface{}) {
|
||||
this.log("[DEBUG]", format, v...)
|
||||
}
|
||||
|
||||
func (this *Logger) Info(format string, v ...interface{}) {
|
||||
this.log("[INFO ]", format, v...)
|
||||
}
|
||||
|
||||
func (this *Logger) Warn(format string, v ...interface{}) {
|
||||
this.log("[WARN ]", format, v...)
|
||||
}
|
||||
|
||||
func (this *Logger) Error(format string, v ...interface{}) {
|
||||
this.log("[ERROR]", format, v...)
|
||||
}
|
||||
|
||||
func (this *Logger) Panic(format string, v ...interface{}) {
|
||||
this.log("[PANIC]", format, v...)
|
||||
panic(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
func (this *Logger) Init() {
|
||||
|
||||
this.openFile()
|
||||
|
||||
//日志需要自我备份,自我维护。明天第一秒触发
|
||||
nextTime := tool.FirstSecondOfDay(tool.Tomorrow())
|
||||
duration := nextTime.Sub(time.Now())
|
||||
|
||||
this.Info("下一次日志维护时间%s 距当前 %ds ", tool.ConvertTimeToDateTimeString(nextTime), duration/time.Second)
|
||||
this.maintainTimer = time.AfterFunc(duration, func() {
|
||||
go tool.SafeMethod(this.maintain)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
//将日志写入到今天的日期中(该方法内必须使用异步方法记录日志,否则会引发死锁)
|
||||
func (this *Logger) maintain() {
|
||||
|
||||
this.Info("每日维护日志")
|
||||
|
||||
this.Lock()
|
||||
defer this.Unlock()
|
||||
|
||||
//首先关闭文件。
|
||||
this.closeFile()
|
||||
|
||||
//日志归类到昨天
|
||||
destPath := tool.GetLogPath() + "/tank-" + tool.Yesterday().Local().Format("2006-01-02") + ".log"
|
||||
|
||||
//直接重命名文件
|
||||
err := os.Rename(this.fileName(), destPath)
|
||||
if err != nil {
|
||||
this.Error("重命名文件出错", err.Error())
|
||||
}
|
||||
|
||||
//再次打开文件
|
||||
this.openFile()
|
||||
|
||||
//准备好下次维护日志的时间。
|
||||
now := time.Now()
|
||||
nextTime := tool.FirstSecondOfDay(tool.Tomorrow())
|
||||
duration := nextTime.Sub(now)
|
||||
this.Info("下次维护时间:%s ", tool.ConvertTimeToDateTimeString(nextTime))
|
||||
this.maintainTimer = time.AfterFunc(duration, func() {
|
||||
go tool.SafeMethod(this.maintain)
|
||||
})
|
||||
}
|
||||
|
||||
//日志名称
|
||||
func (this *Logger) fileName() string {
|
||||
return tool.GetLogPath() + "/tank.log"
|
||||
}
|
||||
|
||||
//打开日志文件
|
||||
func (this *Logger) openFile() {
|
||||
//日志输出到文件中 文件打开后暂时不关闭
|
||||
fmt.Printf("使用日志文件 %s\r\n", this.fileName())
|
||||
f, err := os.OpenFile(this.fileName(), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
panic("日志文件无法正常打开: " + err.Error())
|
||||
}
|
||||
|
||||
this.goLogger = log.New(f, "", log.Ltime|log.Lshortfile)
|
||||
|
||||
this.file = f
|
||||
}
|
||||
|
||||
//关闭日志文件
|
||||
func (this *Logger) closeFile() {
|
||||
if this.file != nil {
|
||||
err := this.file.Close()
|
||||
if err != nil {
|
||||
panic("尝试关闭日志时出错: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Logger) Destroy() {
|
||||
|
||||
this.closeFile()
|
||||
|
||||
if this.maintainTimer != nil {
|
||||
this.maintainTimer.Stop()
|
||||
}
|
||||
|
||||
}
|
35
code/main/main.go
Normal file
35
code/main/main.go
Normal file
@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"log"
|
||||
"net/http"
|
||||
"tank/code"
|
||||
"tank/code/config"
|
||||
"tank/code/logger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
//日志第一优先级保障
|
||||
logger.LOGGER.Init()
|
||||
defer logger.LOGGER.Destroy()
|
||||
|
||||
//装载配置文件,这个决定了是否需要执行安装过程
|
||||
config.CONFIG.Init()
|
||||
|
||||
//全局运行的上下文
|
||||
code.CONTEXT.Init()
|
||||
defer code.CONTEXT.Destroy()
|
||||
|
||||
http.Handle("/", code.CONTEXT.Router)
|
||||
|
||||
logger.LOGGER.Info("App started at http://localhost:%v", config.CONFIG.ServerPort)
|
||||
|
||||
dotPort := fmt.Sprintf(":%v", config.CONFIG.ServerPort)
|
||||
err1 := http.ListenAndServe(dotPort, nil)
|
||||
if err1 != nil {
|
||||
log.Fatal("ListenAndServe: ", err1)
|
||||
}
|
||||
}
|
443
code/matter_controller.go
Normal file
443
code/matter_controller.go
Normal file
@ -0,0 +1,443 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"tank/code/result"
|
||||
)
|
||||
|
||||
type MatterController struct {
|
||||
BaseController
|
||||
matterDao *MatterDao
|
||||
matterService *MatterService
|
||||
downloadTokenDao *DownloadTokenDao
|
||||
imageCacheDao *ImageCacheDao
|
||||
imageCacheService *ImageCacheService
|
||||
}
|
||||
|
||||
//初始化方法 start to develop v3.
|
||||
func (this *MatterController) Init() {
|
||||
this.BaseController.Init()
|
||||
|
||||
//手动装填本实例的Bean. 这里必须要用中间变量方可。
|
||||
b := CONTEXT.GetBean(this.matterDao)
|
||||
if b, ok := b.(*MatterDao); ok {
|
||||
this.matterDao = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.matterService)
|
||||
if b, ok := b.(*MatterService); ok {
|
||||
this.matterService = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.downloadTokenDao)
|
||||
if b, ok := b.(*DownloadTokenDao); ok {
|
||||
this.downloadTokenDao = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.imageCacheDao)
|
||||
if b, ok := b.(*ImageCacheDao); ok {
|
||||
this.imageCacheDao = b
|
||||
}
|
||||
b = CONTEXT.GetBean(this.imageCacheService)
|
||||
if b, ok := b.(*ImageCacheService); ok {
|
||||
this.imageCacheService = b
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//注册自己的路由。
|
||||
func (this *MatterController) RegisterRoutes() map[string]func(writer http.ResponseWriter, request *http.Request) {
|
||||
|
||||
routeMap := make(map[string]func(writer http.ResponseWriter, request *http.Request))
|
||||
|
||||
//每个Controller需要主动注册自己的路由。
|
||||
routeMap["/api/matter/create/directory"] = this.Wrap(this.CreateDirectory, USER_ROLE_USER)
|
||||
routeMap["/api/matter/upload"] = this.Wrap(this.Upload, USER_ROLE_USER)
|
||||
routeMap["/api/matter/crawl"] = this.Wrap(this.Crawl, USER_ROLE_USER)
|
||||
routeMap["/api/matter/delete"] = this.Wrap(this.Delete, USER_ROLE_USER)
|
||||
routeMap["/api/matter/delete/batch"] = this.Wrap(this.DeleteBatch, USER_ROLE_USER)
|
||||
routeMap["/api/matter/rename"] = this.Wrap(this.Rename, USER_ROLE_USER)
|
||||
routeMap["/api/matter/change/privacy"] = this.Wrap(this.ChangePrivacy, USER_ROLE_USER)
|
||||
routeMap["/api/matter/move"] = this.Wrap(this.Move, USER_ROLE_USER)
|
||||
routeMap["/api/matter/detail"] = this.Wrap(this.Detail, USER_ROLE_USER)
|
||||
routeMap["/api/matter/page"] = this.Wrap(this.Page, USER_ROLE_USER)
|
||||
|
||||
return routeMap
|
||||
}
|
||||
|
||||
//查看某个文件的详情。
|
||||
func (this *MatterController) Detail(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
uuid := request.FormValue("uuid")
|
||||
if uuid == "" {
|
||||
panic(result.BadRequest("文件的uuid必填"))
|
||||
}
|
||||
|
||||
matter := this.matterService.Detail(uuid)
|
||||
|
||||
//验证当前之人是否有权限查看这么详细。
|
||||
user := this.checkUser(writer, request)
|
||||
if user.Role != USER_ROLE_ADMINISTRATOR {
|
||||
if matter.UserUuid != user.Uuid {
|
||||
panic("没有权限查看该文件")
|
||||
}
|
||||
}
|
||||
|
||||
return this.Success(matter)
|
||||
|
||||
}
|
||||
|
||||
//按照分页的方式获取某个文件夹下文件和子文件夹的列表,通常情况下只有一页。
|
||||
func (this *MatterController) Page(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
//如果是根目录,那么就传入root.
|
||||
pageStr := request.FormValue("page")
|
||||
pageSizeStr := request.FormValue("pageSize")
|
||||
orderCreateTime := request.FormValue("orderCreateTime")
|
||||
orderUpdateTime := request.FormValue("orderUpdateTime")
|
||||
orderSort := request.FormValue("orderSort")
|
||||
orderTimes := request.FormValue("orderTimes")
|
||||
|
||||
puuid := request.FormValue("puuid")
|
||||
userUuid := request.FormValue("userUuid")
|
||||
name := request.FormValue("name")
|
||||
dir := request.FormValue("dir")
|
||||
alien := request.FormValue("alien")
|
||||
orderDir := request.FormValue("orderDir")
|
||||
orderSize := request.FormValue("orderSize")
|
||||
orderName := request.FormValue("orderName")
|
||||
extensionsStr := request.FormValue("extensions")
|
||||
|
||||
user := this.checkUser(writer, request)
|
||||
if user.Role != USER_ROLE_ADMINISTRATOR {
|
||||
userUuid = user.Uuid
|
||||
}
|
||||
|
||||
var page int
|
||||
if pageStr != "" {
|
||||
page, _ = strconv.Atoi(pageStr)
|
||||
}
|
||||
|
||||
pageSize := 200
|
||||
if pageSizeStr != "" {
|
||||
tmp, err := strconv.Atoi(pageSizeStr)
|
||||
if err == nil {
|
||||
pageSize = tmp
|
||||
}
|
||||
}
|
||||
|
||||
//筛选后缀名
|
||||
var extensions []string
|
||||
if extensionsStr != "" {
|
||||
extensions = strings.Split(extensionsStr, ",")
|
||||
}
|
||||
|
||||
sortArray := []OrderPair{
|
||||
{
|
||||
key: "dir",
|
||||
value: orderDir,
|
||||
},
|
||||
{
|
||||
key: "create_time",
|
||||
value: orderCreateTime,
|
||||
},
|
||||
{
|
||||
key: "update_time",
|
||||
value: orderUpdateTime,
|
||||
},
|
||||
{
|
||||
key: "sort",
|
||||
value: orderSort,
|
||||
},
|
||||
{
|
||||
key: "size",
|
||||
value: orderSize,
|
||||
},
|
||||
{
|
||||
key: "name",
|
||||
value: orderName,
|
||||
},
|
||||
{
|
||||
key: "times",
|
||||
value: orderTimes,
|
||||
},
|
||||
}
|
||||
|
||||
pager := this.matterDao.Page(page, pageSize, puuid, userUuid, name, dir, alien, extensions, sortArray)
|
||||
|
||||
return this.Success(pager)
|
||||
}
|
||||
|
||||
//创建一个文件夹。
|
||||
func (this *MatterController) CreateDirectory(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
puuid := request.FormValue("puuid")
|
||||
name := request.FormValue("name")
|
||||
userUuid := request.FormValue("userUuid")
|
||||
|
||||
//管理员可以指定给某个用户创建文件夹。
|
||||
user := this.checkUser(writer, request)
|
||||
if user.Role != USER_ROLE_ADMINISTRATOR {
|
||||
userUuid = user.Uuid
|
||||
}
|
||||
user = this.userDao.CheckByUuid(userUuid)
|
||||
|
||||
//找到父级matter
|
||||
var dirMatter *Matter
|
||||
if puuid == MATTER_ROOT {
|
||||
dirMatter = NewRootMatter(user)
|
||||
} else {
|
||||
dirMatter = this.matterDao.CheckByUuid(puuid)
|
||||
}
|
||||
|
||||
matter := this.matterService.AtomicCreateDirectory(dirMatter, name, user);
|
||||
return this.Success(matter)
|
||||
}
|
||||
|
||||
//上传文件
|
||||
func (this *MatterController) Upload(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
userUuid := request.FormValue("userUuid")
|
||||
puuid := request.FormValue("puuid")
|
||||
privacyStr := request.FormValue("privacy")
|
||||
file, handler, err := request.FormFile("file")
|
||||
this.PanicError(err)
|
||||
defer func() {
|
||||
err := file.Close()
|
||||
this.PanicError(err)
|
||||
}()
|
||||
|
||||
user := this.checkUser(writer, request)
|
||||
//管理员可以传到指定用户的目录下。
|
||||
if user.Role != USER_ROLE_ADMINISTRATOR {
|
||||
userUuid = user.Uuid
|
||||
}
|
||||
user = this.userDao.CheckByUuid(userUuid)
|
||||
|
||||
privacy := privacyStr == TRUE
|
||||
|
||||
err = request.ParseMultipartForm(32 << 20)
|
||||
this.PanicError(err)
|
||||
|
||||
//对于IE浏览器,filename可能包含了路径。
|
||||
fileName := handler.Filename
|
||||
pos := strings.LastIndex(fileName, "\\")
|
||||
if pos != -1 {
|
||||
fileName = fileName[pos+1:]
|
||||
}
|
||||
pos = strings.LastIndex(fileName, "/")
|
||||
if pos != -1 {
|
||||
fileName = fileName[pos+1:]
|
||||
}
|
||||
|
||||
dirMatter := this.matterDao.CheckWithRootByUuid(puuid, user)
|
||||
|
||||
matter := this.matterService.AtomicUpload(file, user, dirMatter, fileName, privacy)
|
||||
|
||||
return this.Success(matter)
|
||||
}
|
||||
|
||||
//从一个Url中去爬取资源
|
||||
func (this *MatterController) Crawl(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
userUuid := request.FormValue("userUuid")
|
||||
puuid := request.FormValue("puuid")
|
||||
url := request.FormValue("url")
|
||||
privacyStr := request.FormValue("privacy")
|
||||
|
||||
user := this.checkUser(writer, request)
|
||||
if user.Role != USER_ROLE_ADMINISTRATOR {
|
||||
userUuid = user.Uuid
|
||||
} else {
|
||||
if userUuid == "" {
|
||||
userUuid = user.Uuid
|
||||
}
|
||||
}
|
||||
|
||||
user = this.userDao.CheckByUuid(userUuid)
|
||||
|
||||
dirMatter := this.matterDao.CheckWithRootByUuid(puuid, user)
|
||||
|
||||
privacy := false
|
||||
if privacyStr == TRUE {
|
||||
privacy = true
|
||||
}
|
||||
|
||||
if url == "" || (!strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://")) {
|
||||
panic("资源url必填,并且应该以http://或者https://开头")
|
||||
}
|
||||
|
||||
filename := request.FormValue("filename")
|
||||
if filename == "" {
|
||||
panic("文件名必传")
|
||||
}
|
||||
|
||||
matter := this.matterService.AtomicCrawl(url, filename, user, dirMatter, privacy)
|
||||
|
||||
return this.Success(matter)
|
||||
}
|
||||
|
||||
//删除一个文件
|
||||
func (this *MatterController) Delete(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
uuid := request.FormValue("uuid")
|
||||
if uuid == "" {
|
||||
panic(result.BadRequest("文件的uuid必填"))
|
||||
}
|
||||
|
||||
matter := this.matterDao.CheckByUuid(uuid)
|
||||
|
||||
//判断文件的所属人是否正确
|
||||
user := this.checkUser(writer, request)
|
||||
if user.Role != USER_ROLE_ADMINISTRATOR && matter.UserUuid != user.Uuid {
|
||||
panic(result.Unauthorized("没有权限"))
|
||||
}
|
||||
|
||||
this.matterService.AtomicDelete(matter)
|
||||
|
||||
return this.Success("删除成功!")
|
||||
}
|
||||
|
||||
//删除一系列文件。
|
||||
func (this *MatterController) DeleteBatch(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
uuids := request.FormValue("uuids")
|
||||
if uuids == "" {
|
||||
panic(result.BadRequest("文件的uuids必填"))
|
||||
}
|
||||
|
||||
uuidArray := strings.Split(uuids, ",")
|
||||
|
||||
for _, uuid := range uuidArray {
|
||||
|
||||
matter := this.matterDao.FindByUuid(uuid)
|
||||
|
||||
//如果matter已经是nil了,直接跳过
|
||||
if matter == nil {
|
||||
this.logger.Warn("%s 对应的文件记录已经不存在了", uuid)
|
||||
continue
|
||||
}
|
||||
|
||||
//判断文件的所属人是否正确
|
||||
user := this.checkUser(writer, request)
|
||||
if user.Role != USER_ROLE_ADMINISTRATOR && matter.UserUuid != user.Uuid {
|
||||
panic(result.Unauthorized("没有权限"))
|
||||
}
|
||||
|
||||
this.matterService.AtomicDelete(matter)
|
||||
|
||||
}
|
||||
|
||||
return this.Success("删除成功!")
|
||||
}
|
||||
|
||||
//重命名一个文件或一个文件夹
|
||||
func (this *MatterController) Rename(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
uuid := request.FormValue("uuid")
|
||||
name := request.FormValue("name")
|
||||
|
||||
user := this.checkUser(writer, request)
|
||||
|
||||
//找出该文件或者文件夹
|
||||
matter := this.matterDao.CheckByUuid(uuid)
|
||||
|
||||
if user.Role != USER_ROLE_ADMINISTRATOR && matter.UserUuid != user.Uuid {
|
||||
panic(result.Unauthorized("没有权限"))
|
||||
}
|
||||
|
||||
this.matterService.AtomicRename(matter, name, user)
|
||||
|
||||
return this.Success(matter)
|
||||
}
|
||||
|
||||
//改变一个文件的公私有属性
|
||||
func (this *MatterController) ChangePrivacy(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
uuid := request.FormValue("uuid")
|
||||
privacyStr := request.FormValue("privacy")
|
||||
privacy := false
|
||||
if privacyStr == TRUE {
|
||||
privacy = true
|
||||
}
|
||||
//找出该文件或者文件夹
|
||||
matter := this.matterDao.CheckByUuid(uuid)
|
||||
|
||||
if matter.Privacy == privacy {
|
||||
panic("公私有属性没有改变!")
|
||||
}
|
||||
|
||||
//权限验证
|
||||
user := this.checkUser(writer, request)
|
||||
if user.Role != USER_ROLE_ADMINISTRATOR && matter.UserUuid != user.Uuid {
|
||||
panic(result.Unauthorized("没有权限"))
|
||||
}
|
||||
|
||||
matter.Privacy = privacy
|
||||
this.matterDao.Save(matter)
|
||||
|
||||
return this.Success("设置成功")
|
||||
}
|
||||
|
||||
//将一个文件夹或者文件移入到另一个文件夹下。
|
||||
func (this *MatterController) Move(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
srcUuidsStr := request.FormValue("srcUuids")
|
||||
destUuid := request.FormValue("destUuid")
|
||||
userUuid := request.FormValue("userUuid")
|
||||
|
||||
var srcUuids []string
|
||||
//验证参数。
|
||||
if srcUuidsStr == "" {
|
||||
panic(result.BadRequest("srcUuids参数必填"))
|
||||
} else {
|
||||
srcUuids = strings.Split(srcUuidsStr, ",")
|
||||
}
|
||||
|
||||
user := this.checkUser(writer, request)
|
||||
if user.Role != USER_ROLE_ADMINISTRATOR || userUuid == "" {
|
||||
userUuid = user.Uuid
|
||||
}
|
||||
|
||||
user = this.userDao.CheckByUuid(userUuid)
|
||||
|
||||
//验证dest是否有问题
|
||||
var destMatter = this.matterDao.CheckWithRootByUuid(destUuid, user)
|
||||
if !destMatter.Dir {
|
||||
panic(result.BadRequest("目标不是文件夹"))
|
||||
}
|
||||
|
||||
if user.Role != USER_ROLE_ADMINISTRATOR && destMatter.UserUuid != user.Uuid {
|
||||
panic(result.Unauthorized("没有权限"))
|
||||
}
|
||||
|
||||
var srcMatters []*Matter
|
||||
//验证src是否有问题。
|
||||
for _, uuid := range srcUuids {
|
||||
//找出该文件或者文件夹
|
||||
srcMatter := this.matterDao.CheckByUuid(uuid)
|
||||
|
||||
if srcMatter.Puuid == destMatter.Uuid {
|
||||
panic(result.BadRequest("没有进行移动,操作无效!"))
|
||||
}
|
||||
|
||||
//判断同级文件夹中是否有同名的文件
|
||||
count := this.matterDao.CountByUserUuidAndPuuidAndDirAndName(user.Uuid, destMatter.Uuid, srcMatter.Dir, srcMatter.Name)
|
||||
|
||||
if count > 0 {
|
||||
panic(result.BadRequest("【" + srcMatter.Name + "】在目标文件夹已经存在了,操作失败。"))
|
||||
}
|
||||
|
||||
//判断和目标文件夹是否是同一个主人。
|
||||
if srcMatter.UserUuid != destMatter.UserUuid {
|
||||
panic("文件和目标文件夹的拥有者不是同一人")
|
||||
}
|
||||
|
||||
srcMatters = append(srcMatters, srcMatter)
|
||||
}
|
||||
|
||||
this.matterService.AtomicMoveBatch(srcMatters, destMatter)
|
||||
|
||||
return this.Success(nil)
|
||||
}
|
371
code/matter_dao.go
Normal file
371
code/matter_dao.go
Normal file
@ -0,0 +1,371 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/nu7hatch/gouuid"
|
||||
"os"
|
||||
"tank/code/config"
|
||||
"tank/code/result"
|
||||
"tank/code/tool"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MatterDao struct {
|
||||
BaseDao
|
||||
imageCacheDao *ImageCacheDao
|
||||
}
|
||||
|
||||
//初始化方法
|
||||
func (this *MatterDao) Init() {
|
||||
this.BaseDao.Init()
|
||||
|
||||
//手动装填本实例的Bean. 这里必须要用中间变量方可。
|
||||
b := CONTEXT.GetBean(this.imageCacheDao)
|
||||
if b, ok := b.(*ImageCacheDao); ok {
|
||||
this.imageCacheDao = b
|
||||
}
|
||||
}
|
||||
|
||||
//按照Id查询文件
|
||||
func (this *MatterDao) FindByUuid(uuid string) *Matter {
|
||||
|
||||
// Read
|
||||
var matter Matter
|
||||
db := CONTEXT.DB.Where(&Matter{Base: Base{Uuid: uuid}}).First(&matter)
|
||||
if db.Error != nil {
|
||||
if db.Error.Error() == result.DB_ERROR_NOT_FOUND {
|
||||
return nil
|
||||
} else {
|
||||
this.PanicError(db.Error)
|
||||
}
|
||||
}
|
||||
return &matter
|
||||
}
|
||||
|
||||
//按照Id查询文件
|
||||
func (this *MatterDao) CheckByUuid(uuid string) *Matter {
|
||||
matter := this.FindByUuid(uuid)
|
||||
if matter == nil {
|
||||
panic(result.NotFound("%s 对应的matter不存在", uuid))
|
||||
}
|
||||
return matter
|
||||
}
|
||||
|
||||
//按照uuid查找一个文件夹,可能返回root对应的matter.
|
||||
func (this *MatterDao) CheckWithRootByUuid(uuid string, user *User) *Matter {
|
||||
|
||||
if uuid == "" {
|
||||
panic(result.BadRequest("uuid cannot be nil."))
|
||||
}
|
||||
|
||||
var matter *Matter
|
||||
if uuid == MATTER_ROOT {
|
||||
if user == nil {
|
||||
panic(result.BadRequest("user cannot be nil."))
|
||||
}
|
||||
matter = NewRootMatter(user)
|
||||
} else {
|
||||
matter = this.CheckByUuid(uuid)
|
||||
}
|
||||
|
||||
return matter
|
||||
}
|
||||
|
||||
//按照path查找一个matter,可能返回root对应的matter.
|
||||
func (this *MatterDao) CheckWithRootByPath(path string, user *User) *Matter {
|
||||
|
||||
var matter *Matter
|
||||
|
||||
if user == nil {
|
||||
panic(result.BadRequest("user cannot be nil."))
|
||||
}
|
||||
|
||||
//目标文件夹matter
|
||||
if path == "" || path == "/" {
|
||||
matter = NewRootMatter(user)
|
||||
} else {
|
||||
matter = this.checkByUserUuidAndPath(user.Uuid, path)
|
||||
}
|
||||
|
||||
return matter
|
||||
}
|
||||
|
||||
//按照名字查询文件夹
|
||||
func (this *MatterDao) FindByUserUuidAndPuuidAndNameAndDirTrue(userUuid string, puuid string, name string) *Matter {
|
||||
|
||||
var wp = &WherePair{}
|
||||
|
||||
if userUuid != "" {
|
||||
wp = wp.And(&WherePair{Query: "user_uuid = ?", Args: []interface{}{userUuid}})
|
||||
}
|
||||
|
||||
if puuid != "" {
|
||||
wp = wp.And(&WherePair{Query: "puuid = ?", Args: []interface{}{puuid}})
|
||||
}
|
||||
|
||||
if name != "" {
|
||||
wp = wp.And(&WherePair{Query: "name = ?", Args: []interface{}{name}})
|
||||
}
|
||||
|
||||
wp = wp.And(&WherePair{Query: "dir = ?", Args: []interface{}{1}})
|
||||
|
||||
var matter = &Matter{}
|
||||
db := CONTEXT.DB.Model(&Matter{}).Where(wp.Query, wp.Args...).First(matter)
|
||||
|
||||
if db.Error != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return matter
|
||||
}
|
||||
|
||||
//按照id和userUuid来查找。找不到抛异常。
|
||||
func (this *MatterDao) CheckByUuidAndUserUuid(uuid string, userUuid string) *Matter {
|
||||
|
||||
// Read
|
||||
var matter = &Matter{}
|
||||
db := CONTEXT.DB.Where(&Matter{Base: Base{Uuid: uuid}, UserUuid: userUuid}).First(matter)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return matter
|
||||
|
||||
}
|
||||
|
||||
//统计某个用户的某个文件夹下的某个名字的文件(或文件夹)数量。
|
||||
func (this *MatterDao) CountByUserUuidAndPuuidAndDirAndName(userUuid string, puuid string, dir bool, name string) int {
|
||||
|
||||
var matter Matter
|
||||
var count int
|
||||
|
||||
var wp = &WherePair{}
|
||||
|
||||
if puuid != "" {
|
||||
wp = wp.And(&WherePair{Query: "puuid = ?", Args: []interface{}{puuid}})
|
||||
}
|
||||
|
||||
if userUuid != "" {
|
||||
wp = wp.And(&WherePair{Query: "user_uuid = ?", Args: []interface{}{userUuid}})
|
||||
}
|
||||
|
||||
if name != "" {
|
||||
wp = wp.And(&WherePair{Query: "name = ?", Args: []interface{}{name}})
|
||||
}
|
||||
|
||||
wp = wp.And(&WherePair{Query: "dir = ?", Args: []interface{}{dir}})
|
||||
|
||||
db := CONTEXT.DB.
|
||||
Model(&matter).
|
||||
Where(wp.Query, wp.Args...).
|
||||
Count(&count)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
//获取某个用户的某个文件夹下的某个名字的文件(或文件夹)列表
|
||||
func (this *MatterDao) ListByUserUuidAndPuuidAndDirAndName(userUuid string, puuid string, dir bool, name string) []*Matter {
|
||||
|
||||
var matters []*Matter
|
||||
|
||||
db := CONTEXT.DB.
|
||||
Where(Matter{UserUuid: userUuid, Puuid: puuid, Dir: dir, Name: name}).
|
||||
Find(&matters)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return matters
|
||||
}
|
||||
|
||||
//获取某个文件夹下所有的文件和子文件
|
||||
func (this *MatterDao) List(puuid string, userUuid string, sortArray []OrderPair) []*Matter {
|
||||
var matters []*Matter
|
||||
|
||||
db := CONTEXT.DB.Where(Matter{UserUuid: userUuid, Puuid: puuid}).Order(this.GetSortString(sortArray)).Find(&matters)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return matters
|
||||
}
|
||||
|
||||
//获取某个文件夹下所有的文件和子文件
|
||||
func (this *MatterDao) Page(page int, pageSize int, puuid string, userUuid string, name string, dir string, alien string, extensions []string, sortArray []OrderPair) *Pager {
|
||||
|
||||
var wp = &WherePair{}
|
||||
|
||||
if puuid != "" {
|
||||
wp = wp.And(&WherePair{Query: "puuid = ?", Args: []interface{}{puuid}})
|
||||
}
|
||||
|
||||
if userUuid != "" {
|
||||
wp = wp.And(&WherePair{Query: "user_uuid = ?", Args: []interface{}{userUuid}})
|
||||
}
|
||||
|
||||
if name != "" {
|
||||
wp = wp.And(&WherePair{Query: "name LIKE ?", Args: []interface{}{"%" + name + "%"}})
|
||||
}
|
||||
|
||||
if dir == TRUE {
|
||||
wp = wp.And(&WherePair{Query: "dir = ?", Args: []interface{}{1}})
|
||||
} else if dir == FALSE {
|
||||
wp = wp.And(&WherePair{Query: "dir = ?", Args: []interface{}{0}})
|
||||
}
|
||||
|
||||
if alien == TRUE {
|
||||
wp = wp.And(&WherePair{Query: "alien = ?", Args: []interface{}{1}})
|
||||
} else if alien == FALSE {
|
||||
wp = wp.And(&WherePair{Query: "alien = ?", Args: []interface{}{0}})
|
||||
}
|
||||
|
||||
var conditionDB *gorm.DB
|
||||
if extensions != nil && len(extensions) > 0 {
|
||||
var orWp = &WherePair{}
|
||||
|
||||
for _, v := range extensions {
|
||||
orWp = orWp.Or(&WherePair{Query: "name LIKE ?", Args: []interface{}{"%." + v}})
|
||||
}
|
||||
|
||||
conditionDB = CONTEXT.DB.Model(&Matter{}).Where(wp.Query, wp.Args...).Where(orWp.Query, orWp.Args...)
|
||||
} else {
|
||||
conditionDB = CONTEXT.DB.Model(&Matter{}).Where(wp.Query, wp.Args...)
|
||||
}
|
||||
|
||||
count := 0
|
||||
db := conditionDB.Count(&count)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
var matters []*Matter
|
||||
db = conditionDB.Order(this.GetSortString(sortArray)).Offset(page * pageSize).Limit(pageSize).Find(&matters)
|
||||
this.PanicError(db.Error)
|
||||
pager := NewPager(page, pageSize, count, matters)
|
||||
|
||||
return pager
|
||||
}
|
||||
|
||||
//创建
|
||||
func (this *MatterDao) Create(matter *Matter) *Matter {
|
||||
|
||||
timeUUID, _ := uuid.NewV4()
|
||||
matter.Uuid = string(timeUUID.String())
|
||||
matter.CreateTime = time.Now()
|
||||
matter.UpdateTime = time.Now()
|
||||
matter.Sort = time.Now().UnixNano() / 1e6
|
||||
db := CONTEXT.DB.Create(matter)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return matter
|
||||
}
|
||||
|
||||
//修改一个文件
|
||||
func (this *MatterDao) Save(matter *Matter) *Matter {
|
||||
|
||||
matter.UpdateTime = time.Now()
|
||||
db := CONTEXT.DB.Save(matter)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return matter
|
||||
}
|
||||
|
||||
//计数器加一
|
||||
func (this *MatterDao) TimesIncrement(matterUuid string) {
|
||||
db := CONTEXT.DB.Model(&Matter{}).Where("uuid = ?", matterUuid).Update("times", gorm.Expr("times + 1"))
|
||||
this.PanicError(db.Error)
|
||||
}
|
||||
|
||||
//删除一个文件,数据库中删除,物理磁盘上删除。
|
||||
func (this *MatterDao) Delete(matter *Matter) {
|
||||
|
||||
//目录的话递归删除。
|
||||
if matter.Dir {
|
||||
matters := this.List(matter.Uuid, matter.UserUuid, nil)
|
||||
|
||||
for _, f := range matters {
|
||||
this.Delete(f)
|
||||
}
|
||||
|
||||
//删除数据库中文件夹本身
|
||||
db := CONTEXT.DB.Delete(&matter)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
//从磁盘中删除该文件夹。
|
||||
tool.DeleteEmptyDir(matter.AbsolutePath())
|
||||
|
||||
} else {
|
||||
|
||||
//删除数据库中文件记录
|
||||
db := CONTEXT.DB.Delete(&matter)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
//删除对应的缓存图片。
|
||||
this.imageCacheDao.DeleteByMatterUuid(matter.Uuid)
|
||||
|
||||
//删除文件
|
||||
err := os.Remove(matter.AbsolutePath())
|
||||
if err != nil {
|
||||
this.logger.Error("删除磁盘上的文件出错 %s", err.Error())
|
||||
}
|
||||
|
||||
//由于目录和物理结构一一对应,这里不能删除上级文件夹。
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//获取一段时间中,总的数量
|
||||
func (this *MatterDao) CountBetweenTime(startTime time.Time, endTime time.Time) int64 {
|
||||
var count int64
|
||||
db := CONTEXT.DB.Model(&Matter{}).Where("create_time >= ? AND create_time <= ?", startTime, endTime).Count(&count)
|
||||
this.PanicError(db.Error)
|
||||
return count
|
||||
}
|
||||
|
||||
//获取一段时间中文件总大小
|
||||
func (this *MatterDao) SizeBetweenTime(startTime time.Time, endTime time.Time) int64 {
|
||||
var size int64
|
||||
db := CONTEXT.DB.Model(&Matter{}).Where("create_time >= ? AND create_time <= ?", startTime, endTime).Select("SUM(size)")
|
||||
this.PanicError(db.Error)
|
||||
row := db.Row()
|
||||
err := row.Scan(&size)
|
||||
this.PanicError(err)
|
||||
return size
|
||||
}
|
||||
|
||||
//根据userUuid和path来查找
|
||||
func (this *MatterDao) findByUserUuidAndPath(userUuid string, path string) *Matter {
|
||||
|
||||
var wp = &WherePair{Query: "user_uuid = ? AND path = ?", Args: []interface{}{userUuid, path}}
|
||||
|
||||
var matter = &Matter{}
|
||||
db := CONTEXT.DB.Model(&Matter{}).Where(wp.Query, wp.Args...).First(matter)
|
||||
|
||||
if db.Error != nil {
|
||||
if db.Error.Error() == result.DB_ERROR_NOT_FOUND {
|
||||
return nil
|
||||
} else {
|
||||
this.PanicError(db.Error)
|
||||
}
|
||||
}
|
||||
|
||||
return matter
|
||||
}
|
||||
|
||||
//根据userUuid和path来查找
|
||||
func (this *MatterDao) checkByUserUuidAndPath(userUuid string, path string) *Matter {
|
||||
|
||||
if path == "" {
|
||||
panic(result.BadRequest("path 不能为空"))
|
||||
}
|
||||
matter := this.findByUserUuidAndPath(userUuid, path)
|
||||
if matter == nil {
|
||||
panic(result.NotFound("path = %s 不存在", path))
|
||||
}
|
||||
|
||||
return matter
|
||||
}
|
||||
|
||||
//执行清理操作
|
||||
func (this *MatterDao) Cleanup() {
|
||||
this.logger.Info("[MatterDao]执行清理:清除数据库中所有Matter记录。删除磁盘中所有Matter文件。")
|
||||
db := CONTEXT.DB.Where("uuid is not null").Delete(Matter{})
|
||||
this.PanicError(db.Error)
|
||||
|
||||
err := os.RemoveAll(config.CONFIG.MatterPath)
|
||||
this.PanicError(err)
|
||||
|
||||
}
|
84
code/matter_model.go
Normal file
84
code/matter_model.go
Normal file
@ -0,0 +1,84 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"tank/code/config"
|
||||
"tank/code/tool"
|
||||
)
|
||||
|
||||
const (
|
||||
//根目录的uuid
|
||||
MATTER_ROOT = "root"
|
||||
//cache文件夹名称
|
||||
MATTER_CACHE = "cache"
|
||||
//matter名称最大长度
|
||||
MATTER_NAME_MAX_LENGTH = 200
|
||||
//matter文件夹最大深度
|
||||
MATTER_NAME_MAX_DEPTH = 32
|
||||
|
||||
)
|
||||
|
||||
/**
|
||||
* 文件。
|
||||
*/
|
||||
type Matter struct {
|
||||
Base
|
||||
Puuid string `json:"puuid" gorm:"type:char(36);index:idx_puuid"`
|
||||
UserUuid string `json:"userUuid" gorm:"type:char(36);index:idx_uu"`
|
||||
Username string `json:"username" gorm:"type:varchar(45) not null"`
|
||||
Dir bool `json:"dir" gorm:"type:tinyint(1) not null;default:0"`
|
||||
Name string `json:"name" gorm:"type:varchar(255) not null"`
|
||||
Md5 string `json:"md5" gorm:"type:varchar(45)"`
|
||||
Size int64 `json:"size" gorm:"type:bigint(20) not null;default:0"`
|
||||
Privacy bool `json:"privacy" gorm:"type:tinyint(1) not null;default:0"`
|
||||
Path string `json:"path" gorm:"type:varchar(512)"`
|
||||
Times int64 `json:"times" gorm:"type:bigint(20) not null;default:0"`
|
||||
Parent *Matter `json:"parent" gorm:"-"`
|
||||
}
|
||||
|
||||
// set File's table name to be `profiles`
|
||||
func (Matter) TableName() string {
|
||||
return config.TABLE_PREFIX + "matter"
|
||||
}
|
||||
|
||||
// 获取该Matter的绝对路径。path代表的是相对路径。
|
||||
func (this *Matter) AbsolutePath() string {
|
||||
return GetUserFileRootDir(this.Username) + this.Path
|
||||
}
|
||||
|
||||
// 获取该Matter的MimeType
|
||||
func (this *Matter) MimeType() string {
|
||||
return tool.GetMimeType(tool.GetExtension(this.Name))
|
||||
}
|
||||
|
||||
|
||||
//创建一个 ROOT 的matter,主要用于统一化处理移动复制等内容。
|
||||
func NewRootMatter(user *User) *Matter {
|
||||
matter := &Matter{}
|
||||
matter.Uuid = MATTER_ROOT
|
||||
matter.UserUuid = user.Uuid
|
||||
matter.Username = user.Username
|
||||
matter.Dir = true
|
||||
matter.Path = ""
|
||||
matter.CreateTime = user.CreateTime
|
||||
matter.UpdateTime = user.UpdateTime
|
||||
|
||||
return matter
|
||||
}
|
||||
|
||||
//获取到用户文件的根目录。
|
||||
func GetUserFileRootDir(username string) (rootDirPath string) {
|
||||
|
||||
rootDirPath = fmt.Sprintf("%s/%s/%s", config.CONFIG.MatterPath, username, MATTER_ROOT)
|
||||
|
||||
return rootDirPath
|
||||
}
|
||||
|
||||
//获取到用户缓存的根目录。
|
||||
func GetUserCacheRootDir(username string) (rootDirPath string) {
|
||||
|
||||
rootDirPath = fmt.Sprintf("%s/%s/%s", config.CONFIG.MatterPath, username, MATTER_CACHE)
|
||||
|
||||
return rootDirPath
|
||||
}
|
||||
|
690
code/matter_service.go
Normal file
690
code/matter_service.go
Normal file
@ -0,0 +1,690 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"tank/code/download"
|
||||
"tank/code/result"
|
||||
"tank/code/tool"
|
||||
)
|
||||
|
||||
/**
|
||||
* 操作文件的Service
|
||||
* 以 Atomic 开头的方法带有操作锁,这种方法不能被其他的Atomic方法调用,只能提供给外部调用。
|
||||
*/
|
||||
//@Service
|
||||
type MatterService struct {
|
||||
Bean
|
||||
matterDao *MatterDao
|
||||
userDao *UserDao
|
||||
userService *UserService
|
||||
imageCacheDao *ImageCacheDao
|
||||
imageCacheService *ImageCacheService
|
||||
}
|
||||
|
||||
//初始化方法
|
||||
func (this *MatterService) Init() {
|
||||
this.Bean.Init()
|
||||
|
||||
//手动装填本实例的Bean. 这里必须要用中间变量方可。
|
||||
b := CONTEXT.GetBean(this.matterDao)
|
||||
if b, ok := b.(*MatterDao); ok {
|
||||
this.matterDao = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.userDao)
|
||||
if b, ok := b.(*UserDao); ok {
|
||||
this.userDao = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.userService)
|
||||
if b, ok := b.(*UserService); ok {
|
||||
this.userService = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.imageCacheDao)
|
||||
if b, ok := b.(*ImageCacheDao); ok {
|
||||
this.imageCacheDao = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.imageCacheService)
|
||||
if b, ok := b.(*ImageCacheService); ok {
|
||||
this.imageCacheService = b
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//文件下载。支持分片下载
|
||||
func (this *MatterService) DownloadFile(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
filePath string,
|
||||
filename string,
|
||||
withContentDisposition bool) {
|
||||
|
||||
download.DownloadFile(writer, request, filePath, filename, withContentDisposition)
|
||||
}
|
||||
|
||||
//删除文件
|
||||
func (this *MatterService) AtomicDelete(matter *Matter) {
|
||||
|
||||
if matter == nil {
|
||||
panic(result.BadRequest("matter不能为nil"))
|
||||
}
|
||||
|
||||
//操作锁
|
||||
this.userService.MatterLock(matter.UserUuid)
|
||||
defer this.userService.MatterUnlock(matter.UserUuid)
|
||||
|
||||
this.matterDao.Delete(matter)
|
||||
}
|
||||
|
||||
//上传文件
|
||||
func (this *MatterService) Upload(file io.Reader, user *User, dirMatter *Matter, filename string, privacy bool) *Matter {
|
||||
|
||||
if user == nil {
|
||||
panic(result.BadRequest("user cannot be nil."))
|
||||
}
|
||||
|
||||
//验证dirMatter
|
||||
if dirMatter == nil {
|
||||
panic(result.BadRequest("dirMatter cannot be nil."))
|
||||
}
|
||||
|
||||
//文件名不能太长。
|
||||
if len(filename) > MATTER_NAME_MAX_LENGTH {
|
||||
panic(result.BadRequest("文件名不能超过%s", MATTER_NAME_MAX_LENGTH))
|
||||
}
|
||||
|
||||
//文件夹路径
|
||||
dirAbsolutePath := dirMatter.AbsolutePath()
|
||||
dirRelativePath := dirMatter.Path
|
||||
|
||||
count := this.matterDao.CountByUserUuidAndPuuidAndDirAndName(user.Uuid, dirMatter.Uuid, false, filename)
|
||||
if count > 0 {
|
||||
panic(result.BadRequest("该目录下%s已经存在了", filename))
|
||||
}
|
||||
|
||||
//获取文件应该存放在的物理路径的绝对路径和相对路径。
|
||||
fileAbsolutePath := dirAbsolutePath + "/" + filename
|
||||
fileRelativePath := dirRelativePath + "/" + filename
|
||||
|
||||
//创建父文件夹
|
||||
tool.MakeDirAll(dirAbsolutePath)
|
||||
|
||||
//如果文件已经存在了,那么直接覆盖。
|
||||
exist, err := tool.PathExists(fileAbsolutePath)
|
||||
this.PanicError(err)
|
||||
if exist {
|
||||
this.logger.Error("%s已经存在,将其删除", fileAbsolutePath)
|
||||
removeError := os.Remove(fileAbsolutePath)
|
||||
this.PanicError(removeError)
|
||||
}
|
||||
|
||||
destFile, err := os.OpenFile(fileAbsolutePath, os.O_WRONLY|os.O_CREATE, 0777)
|
||||
this.PanicError(err)
|
||||
|
||||
defer func() {
|
||||
err := destFile.Close()
|
||||
this.PanicError(err)
|
||||
}()
|
||||
|
||||
fileSize, err := io.Copy(destFile, file)
|
||||
this.PanicError(err)
|
||||
|
||||
this.logger.Info("上传文件 %s 大小为 %v ", filename, tool.HumanFileSize(fileSize))
|
||||
|
||||
//判断用户自身上传大小的限制。
|
||||
if user.SizeLimit >= 0 {
|
||||
if fileSize > user.SizeLimit {
|
||||
//删除上传过来的内容
|
||||
err = os.Remove(fileAbsolutePath)
|
||||
this.PanicError(err)
|
||||
|
||||
panic(result.BadRequest("文件大小超出限制 %s > %s ", tool.HumanFileSize(user.SizeLimit), tool.HumanFileSize(fileSize)))
|
||||
}
|
||||
}
|
||||
|
||||
//将文件信息存入数据库中。
|
||||
matter := &Matter{
|
||||
Puuid: dirMatter.Uuid,
|
||||
UserUuid: user.Uuid,
|
||||
Username: user.Username,
|
||||
Dir: false,
|
||||
Name: filename,
|
||||
Md5: "",
|
||||
Size: fileSize,
|
||||
Privacy: privacy,
|
||||
Path: fileRelativePath,
|
||||
}
|
||||
matter = this.matterDao.Create(matter)
|
||||
|
||||
return matter
|
||||
}
|
||||
|
||||
//上传文件
|
||||
func (this *MatterService) AtomicUpload(file io.Reader, user *User, dirMatter *Matter, filename string, privacy bool) *Matter {
|
||||
|
||||
if user == nil {
|
||||
panic(result.BadRequest("user cannot be nil."))
|
||||
}
|
||||
|
||||
//操作锁
|
||||
this.userService.MatterLock(user.Uuid)
|
||||
defer this.userService.MatterUnlock(user.Uuid)
|
||||
|
||||
return this.Upload(file, user, dirMatter, filename, privacy)
|
||||
}
|
||||
|
||||
//内部创建文件,不带操作锁。
|
||||
func (this *MatterService) createDirectory(dirMatter *Matter, name string, user *User) *Matter {
|
||||
|
||||
//父级matter必须存在
|
||||
if dirMatter == nil {
|
||||
panic(result.BadRequest("dirMatter必须指定"))
|
||||
}
|
||||
|
||||
//必须是文件夹
|
||||
if !dirMatter.Dir {
|
||||
panic(result.BadRequest("dirMatter必须是文件夹"))
|
||||
}
|
||||
|
||||
if dirMatter.UserUuid != user.Uuid {
|
||||
|
||||
panic(result.BadRequest("dirMatter的userUuid和user不一致"))
|
||||
}
|
||||
|
||||
name = strings.TrimSpace(name)
|
||||
//验证参数。
|
||||
if name == "" {
|
||||
panic(result.BadRequest("name参数必填,并且不能全是空格"))
|
||||
}
|
||||
|
||||
if len(name) > MATTER_NAME_MAX_LENGTH {
|
||||
|
||||
panic(result.BadRequest("name长度不能超过%d", MATTER_NAME_MAX_LENGTH))
|
||||
|
||||
}
|
||||
|
||||
if m, _ := regexp.MatchString(`[<>|*?/\\]`, name); m {
|
||||
panic(result.BadRequest(`名称中不能包含以下特殊符号:< > | * ? / \`))
|
||||
}
|
||||
|
||||
//判断同级文件夹中是否有同名的文件夹
|
||||
count := this.matterDao.CountByUserUuidAndPuuidAndDirAndName(user.Uuid, dirMatter.Uuid, true, name)
|
||||
|
||||
if count > 0 {
|
||||
|
||||
panic(result.BadRequest("%s 已经存在了,请使用其他名称。", name))
|
||||
}
|
||||
|
||||
parts := strings.Split(dirMatter.Path, "/")
|
||||
this.logger.Info("%s的层数:%d", dirMatter.Name, len(parts))
|
||||
|
||||
if len(parts) > 32 {
|
||||
panic(result.BadRequest("文件夹最多%d层", MATTER_NAME_MAX_DEPTH))
|
||||
}
|
||||
|
||||
//绝对路径
|
||||
absolutePath := GetUserFileRootDir(user.Username) + dirMatter.Path + "/" + name
|
||||
|
||||
//相对路径
|
||||
relativePath := dirMatter.Path + "/" + name
|
||||
|
||||
//磁盘中创建文件夹。
|
||||
dirPath := tool.MakeDirAll(absolutePath)
|
||||
this.logger.Info("Create Directory: %s", dirPath)
|
||||
|
||||
//数据库中创建文件夹。
|
||||
matter := &Matter{
|
||||
Puuid: dirMatter.Uuid,
|
||||
UserUuid: user.Uuid,
|
||||
Username: user.Username,
|
||||
Dir: true,
|
||||
Name: name,
|
||||
Path: relativePath,
|
||||
}
|
||||
|
||||
matter = this.matterDao.Create(matter)
|
||||
|
||||
return matter
|
||||
}
|
||||
|
||||
//在dirMatter中创建文件夹 返回刚刚创建的这个文件夹
|
||||
func (this *MatterService) AtomicCreateDirectory(dirMatter *Matter, name string, user *User) *Matter {
|
||||
|
||||
//操作锁
|
||||
this.userService.MatterLock(user.Uuid)
|
||||
defer this.userService.MatterUnlock(user.Uuid)
|
||||
|
||||
matter := this.createDirectory(dirMatter, name, user)
|
||||
|
||||
return matter
|
||||
}
|
||||
|
||||
//处理 移动和复制时可能存在的覆盖问题。
|
||||
func (this *MatterService) handleOverwrite(userUuid string, destinationPath string, overwrite bool) {
|
||||
|
||||
//目标matter。因为有可能已经存在了
|
||||
destMatter := this.matterDao.findByUserUuidAndPath(userUuid, destinationPath)
|
||||
//如果目标matter存在了。
|
||||
if destMatter != nil {
|
||||
//如果目标matter还存在了。
|
||||
if overwrite {
|
||||
//要求覆盖。那么删除。
|
||||
this.matterDao.Delete(destMatter)
|
||||
} else {
|
||||
panic(result.BadRequest("%s已经存在,操作失败!", destMatter.Path))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//将一个srcMatter放置到另一个destMatter(必须为文件夹)下 不关注 overwrite 和 lock.
|
||||
func (this *MatterService) move(srcMatter *Matter, destDirMatter *Matter) {
|
||||
|
||||
if srcMatter == nil {
|
||||
panic(result.BadRequest("srcMatter cannot be nil."))
|
||||
}
|
||||
|
||||
if !destDirMatter.Dir {
|
||||
panic(result.BadRequest("目标必须为文件夹"))
|
||||
}
|
||||
|
||||
if srcMatter.Dir {
|
||||
//如果源是文件夹
|
||||
destAbsolutePath := destDirMatter.AbsolutePath() + "/" + srcMatter.Name
|
||||
srcAbsolutePath := srcMatter.AbsolutePath()
|
||||
|
||||
//物理文件一口气移动
|
||||
err := os.Rename(srcAbsolutePath, destAbsolutePath)
|
||||
this.PanicError(err)
|
||||
|
||||
//修改数据库中信息
|
||||
srcMatter.Puuid = destDirMatter.Uuid
|
||||
srcMatter.Path = destDirMatter.Path + "/" + srcMatter.Name
|
||||
srcMatter = this.matterDao.Save(srcMatter)
|
||||
|
||||
//调整该文件夹下文件的Path.
|
||||
matters := this.matterDao.List(srcMatter.Uuid, srcMatter.UserUuid, nil)
|
||||
for _, m := range matters {
|
||||
this.adjustPath(m, srcMatter)
|
||||
}
|
||||
|
||||
} else {
|
||||
//如果源是普通文件
|
||||
|
||||
destAbsolutePath := destDirMatter.AbsolutePath() + "/" + srcMatter.Name
|
||||
srcAbsolutePath := srcMatter.AbsolutePath()
|
||||
|
||||
//物理文件进行移动
|
||||
err := os.Rename(srcAbsolutePath, destAbsolutePath)
|
||||
this.PanicError(err)
|
||||
|
||||
//删除对应的缓存。
|
||||
this.imageCacheDao.DeleteByMatterUuid(srcMatter.Uuid)
|
||||
|
||||
//修改数据库中信息
|
||||
srcMatter.Puuid = destDirMatter.Uuid
|
||||
srcMatter.Path = destDirMatter.Path + "/" + srcMatter.Name
|
||||
srcMatter = this.matterDao.Save(srcMatter)
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//将一个srcMatter放置到另一个destMatter(必须为文件夹)下
|
||||
func (this *MatterService) AtomicMove(srcMatter *Matter, destDirMatter *Matter, overwrite bool) {
|
||||
|
||||
if srcMatter == nil {
|
||||
panic(result.BadRequest("srcMatter cannot be nil."))
|
||||
}
|
||||
|
||||
//操作锁
|
||||
this.userService.MatterLock(srcMatter.UserUuid)
|
||||
defer this.userService.MatterUnlock(srcMatter.UserUuid)
|
||||
|
||||
if destDirMatter == nil {
|
||||
panic(result.BadRequest("destDirMatter cannot be nil."))
|
||||
}
|
||||
if !destDirMatter.Dir {
|
||||
panic(result.BadRequest("目标必须为文件夹"))
|
||||
}
|
||||
|
||||
//文件夹不能把自己移入到自己中,也不可以移入到自己的子文件夹下。
|
||||
destDirMatter = this.WrapDetail(destDirMatter)
|
||||
tmpMatter := destDirMatter
|
||||
for tmpMatter != nil {
|
||||
if srcMatter.Uuid == tmpMatter.Uuid {
|
||||
panic("文件夹不能把自己移入到自己中,也不可以移入到自己的子文件夹下。")
|
||||
}
|
||||
tmpMatter = tmpMatter.Parent
|
||||
}
|
||||
|
||||
//处理覆盖的问题
|
||||
destinationPath := destDirMatter.Path + "/" + srcMatter.Name
|
||||
this.handleOverwrite(srcMatter.UserUuid, destinationPath, overwrite)
|
||||
|
||||
//做move操作。
|
||||
this.move(srcMatter, destDirMatter)
|
||||
}
|
||||
|
||||
//将一个srcMatter放置到另一个destMatter(必须为文件夹)下
|
||||
func (this *MatterService) AtomicMoveBatch(srcMatters []*Matter, destDirMatter *Matter) {
|
||||
|
||||
if destDirMatter == nil {
|
||||
panic(result.BadRequest("destDirMatter cannot be nil."))
|
||||
}
|
||||
|
||||
//操作锁
|
||||
this.userService.MatterLock(destDirMatter.UserUuid)
|
||||
defer this.userService.MatterUnlock(destDirMatter.UserUuid)
|
||||
|
||||
if srcMatters == nil {
|
||||
panic(result.BadRequest("srcMatters cannot be nil."))
|
||||
}
|
||||
|
||||
if !destDirMatter.Dir {
|
||||
panic(result.BadRequest("目标必须为文件夹"))
|
||||
}
|
||||
|
||||
//文件夹不能把自己移入到自己中,也不可以移入到自己的子文件夹下。
|
||||
destDirMatter = this.WrapDetail(destDirMatter)
|
||||
for _, srcMatter := range srcMatters {
|
||||
|
||||
tmpMatter := destDirMatter
|
||||
for tmpMatter != nil {
|
||||
if srcMatter.Uuid == tmpMatter.Uuid {
|
||||
panic("文件夹不能把自己移入到自己中,也不可以移入到自己的子文件夹下。")
|
||||
}
|
||||
tmpMatter = tmpMatter.Parent
|
||||
}
|
||||
}
|
||||
|
||||
for _, srcMatter := range srcMatters {
|
||||
this.move(srcMatter, destDirMatter)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//内部移动一个文件(提供给Copy调用),无需关心overwrite问题。
|
||||
func (this *MatterService) copy(srcMatter *Matter, destDirMatter *Matter, name string) {
|
||||
|
||||
if srcMatter.Dir {
|
||||
|
||||
//如果源是文件夹
|
||||
|
||||
//在目标地址创建新文件夹。
|
||||
newMatter := &Matter{
|
||||
Puuid: destDirMatter.Uuid,
|
||||
UserUuid: srcMatter.UserUuid,
|
||||
Username: srcMatter.Username,
|
||||
Dir: srcMatter.Dir,
|
||||
Name: name,
|
||||
Md5: "",
|
||||
Size: srcMatter.Size,
|
||||
Privacy: srcMatter.Privacy,
|
||||
Path: destDirMatter.Path + "/" + name,
|
||||
}
|
||||
|
||||
newMatter = this.matterDao.Create(newMatter)
|
||||
|
||||
//复制子文件或文件夹
|
||||
matters := this.matterDao.List(srcMatter.Uuid, srcMatter.UserUuid, nil)
|
||||
for _, m := range matters {
|
||||
this.copy(m, newMatter, m.Name)
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
//如果源是普通文件
|
||||
destAbsolutePath := destDirMatter.AbsolutePath() + "/" + name
|
||||
srcAbsolutePath := srcMatter.AbsolutePath()
|
||||
|
||||
//物理文件进行复制
|
||||
tool.CopyFile(srcAbsolutePath, destAbsolutePath)
|
||||
|
||||
//创建新文件的数据库信息。
|
||||
newMatter := &Matter{
|
||||
Puuid: destDirMatter.Uuid,
|
||||
UserUuid: srcMatter.UserUuid,
|
||||
Username: srcMatter.Username,
|
||||
Dir: srcMatter.Dir,
|
||||
Name: name,
|
||||
Md5: "",
|
||||
Size: srcMatter.Size,
|
||||
Privacy: srcMatter.Privacy,
|
||||
Path: destDirMatter.Path + "/" + name,
|
||||
}
|
||||
newMatter = this.matterDao.Create(newMatter)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//将一个srcMatter复制到另一个destMatter(必须为文件夹)下,名字叫做name
|
||||
func (this *MatterService) AtomicCopy(srcMatter *Matter, destDirMatter *Matter, name string, overwrite bool) {
|
||||
|
||||
if srcMatter == nil {
|
||||
panic(result.BadRequest("srcMatter cannot be nil."))
|
||||
}
|
||||
|
||||
//操作锁
|
||||
this.userService.MatterLock(srcMatter.UserUuid)
|
||||
defer this.userService.MatterUnlock(srcMatter.UserUuid)
|
||||
|
||||
if !destDirMatter.Dir {
|
||||
panic(result.BadRequest("目标必须为文件夹"))
|
||||
}
|
||||
|
||||
destinationPath := destDirMatter.Path + "/" + name
|
||||
this.handleOverwrite(srcMatter.UserUuid, destinationPath, overwrite)
|
||||
|
||||
this.copy(srcMatter, destDirMatter, name)
|
||||
}
|
||||
|
||||
//将一个matter 重命名为 name
|
||||
func (this *MatterService) AtomicRename(matter *Matter, name string, user *User) {
|
||||
|
||||
if user == nil {
|
||||
panic(result.BadRequest("user cannot be nil"))
|
||||
}
|
||||
|
||||
//操作锁
|
||||
this.userService.MatterLock(user.Uuid)
|
||||
defer this.userService.MatterUnlock(user.Uuid)
|
||||
|
||||
//验证参数。
|
||||
if name == "" {
|
||||
panic(result.BadRequest("name参数必填"))
|
||||
}
|
||||
if m, _ := regexp.MatchString(`[<>|*?/\\]`, name); m {
|
||||
panic(result.BadRequest(`名称中不能包含以下特殊符号:< > | * ? / \`))
|
||||
}
|
||||
|
||||
if len(name) > 200 {
|
||||
panic("name长度不能超过200")
|
||||
}
|
||||
|
||||
if name == matter.Name {
|
||||
panic(result.BadRequest("新名称和旧名称一样,操作失败!"))
|
||||
}
|
||||
|
||||
//判断同级文件夹中是否有同名的文件
|
||||
count := this.matterDao.CountByUserUuidAndPuuidAndDirAndName(user.Uuid, matter.Puuid, matter.Dir, name)
|
||||
|
||||
if count > 0 {
|
||||
panic(result.BadRequest("【" + name + "】已经存在了,请使用其他名称。"))
|
||||
}
|
||||
|
||||
if matter.Dir {
|
||||
//如果源是文件夹
|
||||
|
||||
oldAbsolutePath := matter.AbsolutePath()
|
||||
absoluteDirPath := tool.GetDirOfPath(oldAbsolutePath)
|
||||
relativeDirPath := tool.GetDirOfPath(matter.Path)
|
||||
newAbsolutePath := absoluteDirPath + "/" + name
|
||||
|
||||
//物理文件一口气移动
|
||||
err := os.Rename(oldAbsolutePath, newAbsolutePath)
|
||||
this.PanicError(err)
|
||||
|
||||
//修改数据库中信息
|
||||
matter.Name = name
|
||||
matter.Path = relativeDirPath + "/" + name
|
||||
matter = this.matterDao.Save(matter)
|
||||
|
||||
//调整该文件夹下文件的Path.
|
||||
matters := this.matterDao.List(matter.Uuid, matter.UserUuid, nil)
|
||||
for _, m := range matters {
|
||||
this.adjustPath(m, matter)
|
||||
}
|
||||
|
||||
} else {
|
||||
//如果源是普通文件
|
||||
|
||||
oldAbsolutePath := matter.AbsolutePath()
|
||||
absoluteDirPath := tool.GetDirOfPath(oldAbsolutePath)
|
||||
relativeDirPath := tool.GetDirOfPath(matter.Path)
|
||||
newAbsolutePath := absoluteDirPath + "/" + name
|
||||
|
||||
//物理文件进行移动
|
||||
err := os.Rename(oldAbsolutePath, newAbsolutePath)
|
||||
this.PanicError(err)
|
||||
|
||||
//删除对应的缓存。
|
||||
this.imageCacheDao.DeleteByMatterUuid(matter.Uuid)
|
||||
|
||||
//修改数据库中信息
|
||||
matter.Name = name
|
||||
matter.Path = relativeDirPath + "/" + name
|
||||
matter = this.matterDao.Save(matter)
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//根据一个文件夹路径,依次创建,找到最后一个文件夹的matter,如果中途出错,返回err.
|
||||
func (this *MatterService) CreateDirectories(user *User, dirPath string) *Matter {
|
||||
|
||||
if dirPath == "" {
|
||||
panic(`文件夹不能为空`)
|
||||
} else if dirPath[0:1] != "/" {
|
||||
panic(`文件夹必须以/开头`)
|
||||
} else if strings.Index(dirPath, "//") != -1 {
|
||||
panic(`文件夹不能出现连续的//`)
|
||||
} else if m, _ := regexp.MatchString(`[<>|*?\\]`, dirPath); m {
|
||||
panic(`文件夹中不能包含以下特殊符号:< > | * ? \`)
|
||||
}
|
||||
|
||||
if dirPath == "/" {
|
||||
return NewRootMatter(user)
|
||||
}
|
||||
|
||||
//如果最后一个符号为/自动忽略
|
||||
if dirPath[len(dirPath)-1] == '/' {
|
||||
dirPath = dirPath[:len(dirPath)-1]
|
||||
}
|
||||
|
||||
//递归找寻文件的上级目录uuid.
|
||||
folders := strings.Split(dirPath, "/")
|
||||
|
||||
if len(folders) > MATTER_NAME_MAX_DEPTH {
|
||||
panic(result.BadRequest("文件夹最多%d层。", MATTER_NAME_MAX_DEPTH))
|
||||
}
|
||||
|
||||
var dirMatter *Matter
|
||||
for k, name := range folders {
|
||||
|
||||
//split的第一个元素为空字符串,忽略。
|
||||
if k == 0 {
|
||||
dirMatter = NewRootMatter(user)
|
||||
continue
|
||||
}
|
||||
|
||||
dirMatter = this.createDirectory(dirMatter, name, user)
|
||||
}
|
||||
|
||||
return dirMatter
|
||||
}
|
||||
|
||||
//包装某个matter的详情。会把父级依次倒着装进去。如果中途出错,直接抛出异常。
|
||||
func (this *MatterService) WrapDetail(matter *Matter) *Matter {
|
||||
|
||||
if matter == nil {
|
||||
panic(result.BadRequest("matter cannot be nil."))
|
||||
}
|
||||
|
||||
//组装file的内容,展示其父组件。
|
||||
puuid := matter.Puuid
|
||||
tmpMatter := matter
|
||||
for puuid != MATTER_ROOT {
|
||||
pFile := this.matterDao.CheckByUuid(puuid)
|
||||
tmpMatter.Parent = pFile
|
||||
tmpMatter = pFile
|
||||
puuid = pFile.Puuid
|
||||
}
|
||||
|
||||
return matter
|
||||
}
|
||||
|
||||
//获取某个文件的详情,会把父级依次倒着装进去。如果中途出错,直接抛出异常。
|
||||
func (this *MatterService) Detail(uuid string) *Matter {
|
||||
matter := this.matterDao.CheckByUuid(uuid)
|
||||
return this.WrapDetail(matter)
|
||||
}
|
||||
|
||||
//去指定的url中爬文件
|
||||
func (this *MatterService) AtomicCrawl(url string, filename string, user *User, dirMatter *Matter, privacy bool) *Matter {
|
||||
|
||||
if user == nil {
|
||||
panic(result.BadRequest("user cannot be nil."))
|
||||
}
|
||||
|
||||
//操作锁
|
||||
this.userService.MatterLock(user.Uuid)
|
||||
defer this.userService.MatterUnlock(user.Uuid)
|
||||
|
||||
if url == "" || (!strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://")) {
|
||||
panic("资源url必填,并且应该以http://或者https://开头")
|
||||
}
|
||||
|
||||
//从指定的url下载一个文件。参考:https://golangcode.com/download-a-file-from-a-url/
|
||||
resp, err := http.Get(url)
|
||||
this.PanicError(err)
|
||||
|
||||
return this.Upload(resp.Body, user, dirMatter, filename, privacy)
|
||||
}
|
||||
|
||||
//调整一个Matter的path值
|
||||
func (this *MatterService) adjustPath(matter *Matter, parentMatter *Matter) {
|
||||
|
||||
if matter.Dir {
|
||||
//如果源是文件夹
|
||||
|
||||
//首先调整好自己
|
||||
matter.Path = parentMatter.Path + "/" + matter.Name
|
||||
matter = this.matterDao.Save(matter)
|
||||
|
||||
//调整该文件夹下文件的Path.
|
||||
matters := this.matterDao.List(matter.Uuid, matter.UserUuid, nil)
|
||||
for _, m := range matters {
|
||||
this.adjustPath(m, matter)
|
||||
}
|
||||
|
||||
} else {
|
||||
//如果源是普通文件
|
||||
|
||||
//删除该文件的所有缓存
|
||||
this.imageCacheDao.DeleteByMatterUuid(matter.Uuid)
|
||||
|
||||
//调整path
|
||||
matter.Path = parentMatter.Path + "/" + matter.Name
|
||||
matter = this.matterDao.Save(matter)
|
||||
}
|
||||
|
||||
}
|
103
code/preference_controller.go
Normal file
103
code/preference_controller.go
Normal file
@ -0,0 +1,103 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"tank/code/result"
|
||||
"tank/code/tool"
|
||||
)
|
||||
|
||||
type PreferenceController struct {
|
||||
BaseController
|
||||
preferenceDao *PreferenceDao
|
||||
preferenceService *PreferenceService
|
||||
}
|
||||
|
||||
//初始化方法
|
||||
func (this *PreferenceController) Init() {
|
||||
this.BaseController.Init()
|
||||
|
||||
//手动装填本实例的Bean. 这里必须要用中间变量方可。
|
||||
b := CONTEXT.GetBean(this.preferenceDao)
|
||||
if b, ok := b.(*PreferenceDao); ok {
|
||||
this.preferenceDao = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.preferenceService)
|
||||
if b, ok := b.(*PreferenceService); ok {
|
||||
this.preferenceService = b
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//注册自己的路由。
|
||||
func (this *PreferenceController) RegisterRoutes() map[string]func(writer http.ResponseWriter, request *http.Request) {
|
||||
|
||||
routeMap := make(map[string]func(writer http.ResponseWriter, request *http.Request))
|
||||
|
||||
//每个Controller需要主动注册自己的路由。
|
||||
routeMap["/api/preference/fetch"] = this.Wrap(this.Fetch, USER_ROLE_GUEST)
|
||||
routeMap["/api/preference/edit"] = this.Wrap(this.Edit, USER_ROLE_ADMINISTRATOR)
|
||||
routeMap["/api/preference/system_cleanup"] = this.Wrap(this.SystemCleanup, USER_ROLE_ADMINISTRATOR)
|
||||
|
||||
return routeMap
|
||||
}
|
||||
|
||||
//查看某个偏好设置的详情。
|
||||
func (this *PreferenceController) Fetch(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
preference := this.preferenceService.Fetch()
|
||||
|
||||
return this.Success(preference)
|
||||
}
|
||||
|
||||
//修改
|
||||
func (this *PreferenceController) Edit(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
//验证参数。
|
||||
name := request.FormValue("name")
|
||||
if name == "" {
|
||||
panic("name参数必填")
|
||||
}
|
||||
|
||||
logoUrl := request.FormValue("logoUrl")
|
||||
faviconUrl := request.FormValue("faviconUrl")
|
||||
footerLine1 := request.FormValue("footerLine1")
|
||||
footerLine2 := request.FormValue("footerLine2")
|
||||
showAlienStr := request.FormValue("showAlien")
|
||||
|
||||
preference := this.preferenceDao.Fetch()
|
||||
preference.Name = name
|
||||
preference.LogoUrl = logoUrl
|
||||
preference.FaviconUrl = faviconUrl
|
||||
preference.FooterLine1 = footerLine1
|
||||
preference.FooterLine2 = footerLine2
|
||||
if showAlienStr == TRUE {
|
||||
preference.ShowAlien = true
|
||||
} else if showAlienStr == FALSE {
|
||||
preference.ShowAlien = false
|
||||
}
|
||||
|
||||
preference = this.preferenceDao.Save(preference)
|
||||
|
||||
//重置缓存中的偏好
|
||||
this.preferenceService.Reset()
|
||||
|
||||
return this.Success(preference)
|
||||
}
|
||||
|
||||
//清扫系统,所有数据全部丢失。一定要非常慎点,非常慎点!只在系统初始化的时候点击!
|
||||
func (this *PreferenceController) SystemCleanup(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
user := this.checkUser(writer, request)
|
||||
password := request.FormValue("password")
|
||||
|
||||
if !tool.MatchBcrypt(password, user.Password) {
|
||||
panic(result.BadRequest("密码错误,不能执行!"))
|
||||
}
|
||||
|
||||
for _, bean := range CONTEXT.BeanMap {
|
||||
bean.Cleanup()
|
||||
}
|
||||
|
||||
return this.Success("OK")
|
||||
}
|
65
code/preference_dao.go
Normal file
65
code/preference_dao.go
Normal file
@ -0,0 +1,65 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"github.com/nu7hatch/gouuid"
|
||||
"tank/code/result"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PreferenceDao struct {
|
||||
BaseDao
|
||||
}
|
||||
|
||||
//按照Id查询偏好设置
|
||||
func (this *PreferenceDao) Fetch() *Preference {
|
||||
|
||||
// Read
|
||||
var preference = &Preference{}
|
||||
db := CONTEXT.DB.First(preference)
|
||||
if db.Error != nil {
|
||||
|
||||
if db.Error.Error() == result.DB_ERROR_NOT_FOUND {
|
||||
preference.Name = "蓝眼云盘"
|
||||
preference.ShowAlien = true
|
||||
this.Create(preference)
|
||||
return preference
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return preference
|
||||
}
|
||||
|
||||
//创建
|
||||
func (this *PreferenceDao) Create(preference *Preference) *Preference {
|
||||
|
||||
timeUUID, _ := uuid.NewV4()
|
||||
preference.Uuid = string(timeUUID.String())
|
||||
preference.CreateTime = time.Now()
|
||||
preference.UpdateTime = time.Now()
|
||||
preference.Sort = time.Now().UnixNano() / 1e6
|
||||
db := CONTEXT.DB.Create(preference)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return preference
|
||||
}
|
||||
|
||||
//修改一个偏好设置
|
||||
func (this *PreferenceDao) Save(preference *Preference) *Preference {
|
||||
|
||||
preference.UpdateTime = time.Now()
|
||||
db := CONTEXT.DB.Save(preference)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return preference
|
||||
}
|
||||
|
||||
|
||||
//执行清理操作
|
||||
func (this *PreferenceDao) Cleanup() {
|
||||
|
||||
this.logger.Info("[PreferenceDao]执行清理:清除数据库中所有Preference记录。")
|
||||
db := CONTEXT.DB.Where("uuid is not null").Delete(Preference{})
|
||||
this.PanicError(db.Error)
|
||||
}
|
18
code/preference_model.go
Normal file
18
code/preference_model.go
Normal file
@ -0,0 +1,18 @@
|
||||
package code
|
||||
|
||||
import "tank/code/config"
|
||||
|
||||
type Preference struct {
|
||||
Base
|
||||
Name string `json:"name" gorm:"type:varchar(45)"`
|
||||
LogoUrl string `json:"logoUrl" gorm:"type:varchar(255)"`
|
||||
FaviconUrl string `json:"faviconUrl" gorm:"type:varchar(255)"`
|
||||
FooterLine1 string `json:"footerLine1" gorm:"type:varchar(1024)"`
|
||||
FooterLine2 string `json:"footerLine2" gorm:"type:varchar(1024)"`
|
||||
ShowAlien bool `json:"showAlien" gorm:"type:tinyint(1) not null;default:1"`
|
||||
}
|
||||
|
||||
// set File's table name to be `profiles`
|
||||
func (this *Preference) TableName() string {
|
||||
return config.TABLE_PREFIX + "preference"
|
||||
}
|
45
code/preference_service.go
Normal file
45
code/preference_service.go
Normal file
@ -0,0 +1,45 @@
|
||||
package code
|
||||
|
||||
//@Service
|
||||
type PreferenceService struct {
|
||||
Bean
|
||||
preferenceDao *PreferenceDao
|
||||
preference *Preference
|
||||
}
|
||||
|
||||
//初始化方法
|
||||
func (this *PreferenceService) Init() {
|
||||
this.Bean.Init()
|
||||
|
||||
//手动装填本实例的Bean. 这里必须要用中间变量方可。
|
||||
b := CONTEXT.GetBean(this.preferenceDao)
|
||||
if b, ok := b.(*PreferenceDao); ok {
|
||||
this.preferenceDao = b
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//获取单例的配置。
|
||||
func (this *PreferenceService) Fetch() *Preference {
|
||||
|
||||
if this.preference == nil {
|
||||
this.preference = this.preferenceDao.Fetch()
|
||||
}
|
||||
|
||||
return this.preference
|
||||
}
|
||||
|
||||
//清空单例配置。
|
||||
func (this *PreferenceService) Reset() {
|
||||
|
||||
this.preference = nil
|
||||
|
||||
}
|
||||
|
||||
//执行清理操作
|
||||
func (this *PreferenceService) Cleanup() {
|
||||
|
||||
this.logger.Info("[PreferenceService]执行清理:重置缓存中的preference。")
|
||||
|
||||
this.Reset()
|
||||
}
|
122
code/result/web_result.go
Normal file
122
code/result/web_result.go
Normal file
@ -0,0 +1,122 @@
|
||||
package result
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type WebResult struct {
|
||||
Code string `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func (this *WebResult) Error() string {
|
||||
return this.Msg
|
||||
}
|
||||
|
||||
type CodeWrapper struct {
|
||||
Code string
|
||||
HttpStatus int
|
||||
Description string
|
||||
}
|
||||
|
||||
var (
|
||||
CODE_WRAPPER_OK = &CodeWrapper{Code: "OK", HttpStatus: http.StatusOK, Description: "成功"}
|
||||
CODE_WRAPPER_BAD_REQUEST = &CodeWrapper{Code: "BAD_REQUEST", HttpStatus: http.StatusBadRequest, Description: "请求不合法"}
|
||||
CODE_WRAPPER_CAPTCHA_ERROR = &CodeWrapper{Code: "CAPTCHA_ERROR", HttpStatus: http.StatusBadRequest, Description: "验证码错误"}
|
||||
CODE_WRAPPER_NEED_CAPTCHA = &CodeWrapper{Code: "NEED_CAPTCHA", HttpStatus: http.StatusBadRequest, Description: "验证码必填"}
|
||||
CODE_WRAPPER_USERNAME_PASSWORD_ERROR = &CodeWrapper{Code: "USERNAME_PASSWORD_ERROR", HttpStatus: http.StatusBadRequest, Description: "用户名或密码错误"}
|
||||
CODE_WRAPPER_PARAMS_ERROR = &CodeWrapper{Code: "PARAMS_ERROR", HttpStatus: http.StatusBadRequest, Description: "用户名或密码错误"}
|
||||
CODE_WRAPPER_LOGIN = &CodeWrapper{Code: "LOGIN", HttpStatus: http.StatusUnauthorized, Description: "未登录,禁止访问"}
|
||||
CODE_WRAPPER_LOGIN_EXPIRE = &CodeWrapper{Code: "LOGIN_EXPIRE", HttpStatus: http.StatusUnauthorized, Description: "登录过期,请重新登录"}
|
||||
CODE_WRAPPER_USER_DISABLED = &CodeWrapper{Code: "USER_DISABLED", HttpStatus: http.StatusForbidden, Description: "账户被禁用,禁止访问"}
|
||||
CODE_WRAPPER_UNAUTHORIZED = &CodeWrapper{Code: "UNAUTHORIZED", HttpStatus: http.StatusUnauthorized, Description: "没有权限,禁止访问"}
|
||||
CODE_WRAPPER_NOT_FOUND = &CodeWrapper{Code: "NOT_FOUND", HttpStatus: http.StatusNotFound, Description: "内容不存在"}
|
||||
CODE_WRAPPER_RANGE_NOT_SATISFIABLE = &CodeWrapper{Code: "RANGE_NOT_SATISFIABLE", HttpStatus: http.StatusRequestedRangeNotSatisfiable, Description: "文件范围读取错误"}
|
||||
CODE_WRAPPER_NOT_INSTALLED = &CodeWrapper{Code: "NOT_INSTALLED", HttpStatus: http.StatusInternalServerError, Description: "系统尚未安装"}
|
||||
CODE_WRAPPER_SERVER = &CodeWrapper{Code: "SERVER", HttpStatus: http.StatusInternalServerError, Description: "服务器出错"}
|
||||
CODE_WRAPPER_UNKNOWN = &CodeWrapper{Code: "UNKNOWN", HttpStatus: http.StatusInternalServerError, Description: "服务器未知错误"}
|
||||
)
|
||||
|
||||
//根据 CodeWrapper来获取对应的HttpStatus
|
||||
func FetchHttpStatus(code string) int {
|
||||
if code == CODE_WRAPPER_OK.Code {
|
||||
return CODE_WRAPPER_OK.HttpStatus
|
||||
} else if code == CODE_WRAPPER_BAD_REQUEST.Code {
|
||||
return CODE_WRAPPER_BAD_REQUEST.HttpStatus
|
||||
} else if code == CODE_WRAPPER_CAPTCHA_ERROR.Code {
|
||||
return CODE_WRAPPER_CAPTCHA_ERROR.HttpStatus
|
||||
} else if code == CODE_WRAPPER_NEED_CAPTCHA.Code {
|
||||
return CODE_WRAPPER_NEED_CAPTCHA.HttpStatus
|
||||
} else if code == CODE_WRAPPER_USERNAME_PASSWORD_ERROR.Code {
|
||||
return CODE_WRAPPER_USERNAME_PASSWORD_ERROR.HttpStatus
|
||||
} else if code == CODE_WRAPPER_PARAMS_ERROR.Code {
|
||||
return CODE_WRAPPER_PARAMS_ERROR.HttpStatus
|
||||
} else if code == CODE_WRAPPER_LOGIN.Code {
|
||||
return CODE_WRAPPER_LOGIN.HttpStatus
|
||||
} else if code == CODE_WRAPPER_LOGIN_EXPIRE.Code {
|
||||
return CODE_WRAPPER_LOGIN_EXPIRE.HttpStatus
|
||||
} else if code == CODE_WRAPPER_USER_DISABLED.Code {
|
||||
return CODE_WRAPPER_USER_DISABLED.HttpStatus
|
||||
} else if code == CODE_WRAPPER_UNAUTHORIZED.Code {
|
||||
return CODE_WRAPPER_UNAUTHORIZED.HttpStatus
|
||||
} else if code == CODE_WRAPPER_NOT_FOUND.Code {
|
||||
return CODE_WRAPPER_NOT_FOUND.HttpStatus
|
||||
} else if code == CODE_WRAPPER_RANGE_NOT_SATISFIABLE.Code {
|
||||
return CODE_WRAPPER_RANGE_NOT_SATISFIABLE.HttpStatus
|
||||
} else if code == CODE_WRAPPER_NOT_INSTALLED.Code {
|
||||
return CODE_WRAPPER_NOT_INSTALLED.HttpStatus
|
||||
} else if code == CODE_WRAPPER_SERVER.Code {
|
||||
return CODE_WRAPPER_SERVER.HttpStatus
|
||||
} else {
|
||||
return CODE_WRAPPER_UNKNOWN.HttpStatus
|
||||
}
|
||||
}
|
||||
|
||||
func ConstWebResult(codeWrapper *CodeWrapper) *WebResult {
|
||||
|
||||
wr := &WebResult{
|
||||
Code: codeWrapper.Code,
|
||||
Msg: codeWrapper.Description,
|
||||
}
|
||||
return wr
|
||||
}
|
||||
|
||||
func CustomWebResult(codeWrapper *CodeWrapper, description string) *WebResult {
|
||||
|
||||
wr := &WebResult{
|
||||
Code: codeWrapper.Code,
|
||||
Msg: description,
|
||||
}
|
||||
return wr
|
||||
}
|
||||
|
||||
//请求参数有问题
|
||||
func BadRequest(format string, v ...interface{}) *WebResult {
|
||||
return CustomWebResult(CODE_WRAPPER_BAD_REQUEST, fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
//没有权限
|
||||
func Unauthorized(format string, v ...interface{}) *WebResult {
|
||||
return CustomWebResult(CODE_WRAPPER_UNAUTHORIZED, fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
//没有找到
|
||||
func NotFound(format string, v ...interface{}) *WebResult {
|
||||
return CustomWebResult(CODE_WRAPPER_NOT_FOUND, fmt.Sprintf(format, v...))
|
||||
|
||||
}
|
||||
|
||||
//服务器内部出问题
|
||||
func Server(format string, v ...interface{}) *WebResult {
|
||||
return CustomWebResult(CODE_WRAPPER_SERVER, fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
//所有的数据库错误情况
|
||||
var (
|
||||
DB_ERROR_DUPLICATE_KEY = "Error 1062: Duplicate entry"
|
||||
DB_ERROR_NOT_FOUND = "record not found"
|
||||
DB_TOO_MANY_CONNECTIONS = "Error 1040: Too many connections"
|
||||
DB_BAD_CONNECTION = "driver: bad connection"
|
||||
)
|
206
code/router.go
Normal file
206
code/router.go
Normal file
@ -0,0 +1,206 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/json-iterator/go"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"tank/code/config"
|
||||
"tank/code/logger"
|
||||
"tank/code/result"
|
||||
"tank/code/tool"
|
||||
"time"
|
||||
)
|
||||
|
||||
//用于处理所有前来的请求
|
||||
type Router struct {
|
||||
installController *InstallController
|
||||
footprintService *FootprintService
|
||||
userService *UserService
|
||||
routeMap map[string]func(writer http.ResponseWriter, request *http.Request)
|
||||
installRouteMap map[string]func(writer http.ResponseWriter, request *http.Request)
|
||||
}
|
||||
|
||||
//构造方法
|
||||
func NewRouter() *Router {
|
||||
router := &Router{
|
||||
routeMap: make(map[string]func(writer http.ResponseWriter, request *http.Request)),
|
||||
installRouteMap: make(map[string]func(writer http.ResponseWriter, request *http.Request)),
|
||||
}
|
||||
|
||||
//installController.
|
||||
b := CONTEXT.GetBean(router.installController)
|
||||
if b, ok := b.(*InstallController); ok {
|
||||
router.installController = b
|
||||
}
|
||||
|
||||
//装载userService.
|
||||
b = CONTEXT.GetBean(router.userService)
|
||||
if b, ok := b.(*UserService); ok {
|
||||
router.userService = b
|
||||
}
|
||||
|
||||
//装载footprintService
|
||||
b = CONTEXT.GetBean(router.footprintService)
|
||||
if b, ok := b.(*FootprintService); ok {
|
||||
router.footprintService = b
|
||||
}
|
||||
|
||||
//将Controller中的路由规则装载进来,InstallController中的除外
|
||||
for _, controller := range CONTEXT.ControllerMap {
|
||||
|
||||
if controller == router.installController {
|
||||
routes := controller.RegisterRoutes()
|
||||
for k, v := range routes {
|
||||
router.installRouteMap[k] = v
|
||||
}
|
||||
} else {
|
||||
routes := controller.RegisterRoutes()
|
||||
for k, v := range routes {
|
||||
router.routeMap[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return router
|
||||
|
||||
}
|
||||
|
||||
//全局的异常捕获
|
||||
func (this *Router) GlobalPanicHandler(writer http.ResponseWriter, request *http.Request, startTime time.Time) {
|
||||
if err := recover(); err != nil {
|
||||
|
||||
logger.LOGGER.Error("错误: %v", err)
|
||||
|
||||
var webResult *result.WebResult = nil
|
||||
if value, ok := err.(string); ok {
|
||||
//一个字符串,默认是请求错误。
|
||||
webResult = result.CustomWebResult(result.CODE_WRAPPER_BAD_REQUEST, value)
|
||||
} else if value, ok := err.(*result.WebResult); ok {
|
||||
//一个WebResult对象
|
||||
webResult = value
|
||||
} else if value, ok := err.(*result.CodeWrapper); ok {
|
||||
//一个WebResult对象
|
||||
webResult = result.ConstWebResult(value)
|
||||
} else if value, ok := err.(error); ok {
|
||||
//一个普通的错误对象
|
||||
webResult = result.CustomWebResult(result.CODE_WRAPPER_UNKNOWN, value.Error())
|
||||
} else {
|
||||
//其他不能识别的内容
|
||||
webResult = result.ConstWebResult(result.CODE_WRAPPER_UNKNOWN)
|
||||
}
|
||||
|
||||
//修改http code码
|
||||
writer.WriteHeader(result.FetchHttpStatus(webResult.Code))
|
||||
|
||||
//输出的是json格式 返回的内容申明是json,utf-8
|
||||
writer.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
//用json的方式输出返回值。
|
||||
b, _ := jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(webResult)
|
||||
|
||||
//写到输出流中
|
||||
_, err := fmt.Fprintf(writer, string(b))
|
||||
if err != nil {
|
||||
fmt.Printf("输出结果时出错了\n")
|
||||
}
|
||||
|
||||
//错误情况记录。
|
||||
go tool.SafeMethod(func() {
|
||||
this.footprintService.Trace(writer, request, time.Now().Sub(startTime), false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//让Router具有处理请求的功能。
|
||||
func (this *Router) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
//每个请求的入口在这里
|
||||
//全局异常处理。
|
||||
defer this.GlobalPanicHandler(writer, request, startTime)
|
||||
|
||||
path := request.URL.Path
|
||||
if strings.HasPrefix(path, "/api") {
|
||||
|
||||
//对于IE浏览器,会自动缓存,因此设置不缓存Header.
|
||||
writer.Header().Set("Pragma", "No-cache")
|
||||
writer.Header().Set("Cache-Control", "no-cache")
|
||||
writer.Header().Set("Expires", "0")
|
||||
|
||||
|
||||
if config.CONFIG.Installed {
|
||||
//已安装的模式
|
||||
|
||||
//统一处理用户的身份信息。
|
||||
this.userService.bootstrap(writer, request)
|
||||
|
||||
if handler, ok := this.routeMap[path]; ok {
|
||||
handler(writer, request)
|
||||
} else {
|
||||
//直接将请求扔给每个controller,看看他们能不能处理,如果都不能处理,那就抛出找不到的错误
|
||||
canHandle := false
|
||||
for _, controller := range CONTEXT.ControllerMap {
|
||||
if handler, exist := controller.HandleRoutes(writer, request); exist {
|
||||
canHandle = true
|
||||
handler(writer, request)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !canHandle {
|
||||
panic(result.CustomWebResult(result.CODE_WRAPPER_NOT_FOUND, fmt.Sprintf("没有找到能够处理%s的方法", path)))
|
||||
}
|
||||
}
|
||||
|
||||
//正常的访问记录会落到这里。
|
||||
go tool.SafeMethod(func() {
|
||||
this.footprintService.Trace(writer, request, time.Now().Sub(startTime), true)
|
||||
})
|
||||
|
||||
} else {
|
||||
//未安装模式
|
||||
if handler, ok := this.installRouteMap[path]; ok {
|
||||
handler(writer, request)
|
||||
} else {
|
||||
panic(result.ConstWebResult(result.CODE_WRAPPER_NOT_INSTALLED))
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
//当作静态资源处理。默认从当前文件下面的static文件夹中取东西。
|
||||
dir := tool.GetHtmlPath()
|
||||
|
||||
requestURI := request.RequestURI
|
||||
if requestURI == "" || request.RequestURI == "/" {
|
||||
requestURI = "index.html"
|
||||
}
|
||||
|
||||
filePath := dir + requestURI
|
||||
exists, _ := tool.PathExists(filePath)
|
||||
if !exists {
|
||||
filePath = dir + "/index.html"
|
||||
exists, _ = tool.PathExists(filePath)
|
||||
if !exists {
|
||||
panic(fmt.Sprintf("404 not found:%s", filePath))
|
||||
}
|
||||
}
|
||||
|
||||
writer.Header().Set("Content-Type", tool.GetMimeType(tool.GetExtension(filePath)))
|
||||
|
||||
diskFile, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
panic("cannot get file.")
|
||||
}
|
||||
defer diskFile.Close()
|
||||
_, err = io.Copy(writer, diskFile)
|
||||
if err != nil {
|
||||
panic("cannot get file.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
78
code/session_dao.go
Normal file
78
code/session_dao.go
Normal file
@ -0,0 +1,78 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
|
||||
"github.com/nu7hatch/gouuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SessionDao struct {
|
||||
BaseDao
|
||||
}
|
||||
|
||||
//按照Id查询session.
|
||||
func (this *SessionDao) FindByUuid(uuid string) *Session {
|
||||
|
||||
// Read
|
||||
var session = &Session{}
|
||||
db := CONTEXT.DB.Where(&Session{Base: Base{Uuid: uuid}}).First(session)
|
||||
if db.Error != nil {
|
||||
return nil
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
//按照Id查询session.
|
||||
func (this *SessionDao) CheckByUuid(uuid string) *Session {
|
||||
|
||||
// Read
|
||||
var session = &Session{}
|
||||
db := CONTEXT.DB.Where(&Session{Base: Base{Uuid: uuid}}).First(session)
|
||||
this.PanicError(db.Error)
|
||||
return session
|
||||
}
|
||||
|
||||
//创建一个session并且持久化到数据库中。
|
||||
func (this *SessionDao) Create(session *Session) *Session {
|
||||
|
||||
timeUUID, _ := uuid.NewV4()
|
||||
session.Uuid = string(timeUUID.String())
|
||||
session.CreateTime = time.Now()
|
||||
session.UpdateTime = time.Now()
|
||||
session.Sort = time.Now().UnixNano() / 1e6
|
||||
db := CONTEXT.DB.Create(session)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
|
||||
//修改一个session
|
||||
func (this *SessionDao) Save(session *Session) *Session {
|
||||
|
||||
session.UpdateTime = time.Now()
|
||||
db := CONTEXT.DB.Save(session)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
|
||||
func (this *SessionDao) Delete(uuid string) {
|
||||
|
||||
session := this.CheckByUuid(uuid)
|
||||
|
||||
session.ExpireTime = time.Now()
|
||||
db := CONTEXT.DB.Delete(session)
|
||||
|
||||
this.PanicError(db.Error)
|
||||
|
||||
}
|
||||
|
||||
|
||||
//执行清理操作
|
||||
func (this *SessionDao) Cleanup() {
|
||||
this.logger.Info("[SessionDao]执行清理:清除数据库中所有Session记录。")
|
||||
db := CONTEXT.DB.Where("uuid is not null").Delete(Session{})
|
||||
this.PanicError(db.Error)
|
||||
}
|
18
code/session_model.go
Normal file
18
code/session_model.go
Normal file
@ -0,0 +1,18 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"tank/code/config"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
Base
|
||||
UserUuid string `json:"userUuid" gorm:"type:char(36)"`
|
||||
Ip string `json:"ip" gorm:"type:varchar(128) not null"`
|
||||
ExpireTime time.Time `json:"expireTime" gorm:"type:timestamp not null;default:'2018-01-01 00:00:00'"`
|
||||
}
|
||||
|
||||
// set User's table name to be `profiles`
|
||||
func (this *Session) TableName() string {
|
||||
return config.TABLE_PREFIX + "session"
|
||||
}
|
33
code/session_service.go
Normal file
33
code/session_service.go
Normal file
@ -0,0 +1,33 @@
|
||||
package code
|
||||
|
||||
//@Service
|
||||
type SessionService struct {
|
||||
Bean
|
||||
userDao *UserDao
|
||||
sessionDao *SessionDao
|
||||
}
|
||||
|
||||
//初始化方法
|
||||
func (this *SessionService) Init() {
|
||||
this.Bean.Init()
|
||||
|
||||
//手动装填本实例的Bean. 这里必须要用中间变量方可。
|
||||
b := CONTEXT.GetBean(this.userDao)
|
||||
if b, ok := b.(*UserDao); ok {
|
||||
this.userDao = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.sessionDao)
|
||||
if b, ok := b.(*SessionDao); ok {
|
||||
this.sessionDao = b
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//执行清理操作
|
||||
func (this *SessionService) Cleanup() {
|
||||
|
||||
this.logger.Info("[SessionService]执行清理:清除缓存中所有Session记录,共%d条。", CONTEXT.SessionCache.Count())
|
||||
|
||||
CONTEXT.SessionCache.Truncate()
|
||||
}
|
48
code/sql_builder.go
Normal file
48
code/sql_builder.go
Normal file
@ -0,0 +1,48 @@
|
||||
package code
|
||||
|
||||
type OrderPair struct {
|
||||
key string
|
||||
value string
|
||||
}
|
||||
|
||||
type WherePair struct {
|
||||
Query string
|
||||
Args []interface{}
|
||||
}
|
||||
|
||||
func (this *WherePair) And(where *WherePair) *WherePair {
|
||||
if this.Query == "" {
|
||||
return where
|
||||
} else {
|
||||
return &WherePair{Query: this.Query + " AND " + where.Query, Args: append(this.Args, where.Args...)}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (this *WherePair) Or(where *WherePair) *WherePair {
|
||||
if this.Query == "" {
|
||||
return where
|
||||
} else {
|
||||
return &WherePair{Query: this.Query + " OR " + where.Query, Args: append(this.Args, where.Args...)}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//根据一个sortMap,获取到order字符串
|
||||
func (this *BaseDao) GetSortString(sortArray []OrderPair) string {
|
||||
|
||||
if sortArray == nil || len(sortArray) == 0 {
|
||||
return ""
|
||||
}
|
||||
str := ""
|
||||
for _, pair := range sortArray {
|
||||
if pair.value == "DESC" || pair.value == "ASC" {
|
||||
if str != "" {
|
||||
str = str + ","
|
||||
}
|
||||
str = str + " " + pair.key + " " + pair.value
|
||||
}
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
103
code/test/dav_test.go
Normal file
103
code/test/dav_test.go
Normal file
@ -0,0 +1,103 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"tank/code/dav"
|
||||
"tank/code/dav/xml"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestXmlDecoder(t *testing.T) {
|
||||
|
||||
propfind := &dav.Propfind{}
|
||||
|
||||
str := `
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<D:propfind xmlns:D="DAV:">
|
||||
<D:prop>
|
||||
<D:resourcetype />
|
||||
<D:getcontentlength />
|
||||
<D:creationdate />
|
||||
<D:getlastmodified />
|
||||
</D:prop>
|
||||
</D:propfind>
|
||||
`
|
||||
|
||||
reader := bytes.NewReader([]byte(str))
|
||||
|
||||
err := xml.NewDecoder(reader).Decode(propfind)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
|
||||
resultMap := make(map[string]bool)
|
||||
|
||||
resultMap[`propfind.XMLName.Space == "DAV:"`] = propfind.XMLName.Space == "DAV:"
|
||||
|
||||
resultMap[`propfind.XMLName.Local == "propfind"`] = propfind.XMLName.Local == "propfind"
|
||||
|
||||
resultMap[`len(propfind.Prop) == 4`] = len(propfind.Prop) == 4
|
||||
|
||||
resultMap[`propfind.Prop[0]`] = propfind.Prop[0].Space == "DAV:" && propfind.Prop[0].Local == "resourcetype"
|
||||
resultMap[`propfind.Prop[1]`] = propfind.Prop[1].Space == "DAV:" && propfind.Prop[1].Local == "getcontentlength"
|
||||
resultMap[`propfind.Prop[2]`] = propfind.Prop[2].Space == "DAV:" && propfind.Prop[2].Local == "creationdate"
|
||||
resultMap[`propfind.Prop[3]`] = propfind.Prop[3].Space == "DAV:" && propfind.Prop[3].Local == "getlastmodified"
|
||||
|
||||
for k, v := range resultMap {
|
||||
if !v {
|
||||
t.Errorf(" %s error", k)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("[%v] pass!", time.Now())
|
||||
|
||||
}
|
||||
|
||||
func TestXmlEncoder(t *testing.T) {
|
||||
|
||||
writer := &bytes.Buffer{}
|
||||
|
||||
response := &dav.Response{
|
||||
XMLName: xml.Name{Space: "DAV:", Local: "response"},
|
||||
Href: []string{"/api/dav"},
|
||||
Propstat: []dav.SubPropstat{
|
||||
{
|
||||
Prop: []dav.Property{
|
||||
{
|
||||
XMLName: xml.Name{Space: "DAV:", Local: "resourcetype"},
|
||||
InnerXML: []byte(`<D:collection xmlns:D="DAV:"/>`),
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{Space: "DAV:", Local: "getlastmodified"},
|
||||
InnerXML: []byte(`Mon, 22 Apr 2019 06:38:36 GMT`),
|
||||
},
|
||||
},
|
||||
Status: "HTTP/1.1 200 OK",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := xml.NewEncoder(writer).Encode(response)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
|
||||
bs := writer.Bytes()
|
||||
|
||||
str := string(bs)
|
||||
|
||||
resultMap := make(map[string]bool)
|
||||
|
||||
resultMap["equal"] = str == `<D:response><D:href>/api/dav</D:href><D:propstat><D:prop><D:resourcetype><D:collection xmlns:D="DAV:"/></D:resourcetype><D:getlastmodified>Mon, 22 Apr 2019 06:38:36 GMT</D:getlastmodified></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response>`
|
||||
|
||||
for k, v := range resultMap {
|
||||
if !v {
|
||||
t.Errorf("%s error", k)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("[%v] pass!", time.Now())
|
||||
|
||||
}
|
15
code/test/main_test.go
Normal file
15
code/test/main_test.go
Normal file
@ -0,0 +1,15 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
||||
func TestHello(t *testing.T) {
|
||||
|
||||
split := strings.Split("good", "/")
|
||||
fmt.Printf("%v", split)
|
||||
|
||||
}
|
30
code/tool/util_encode.go
Normal file
30
code/tool/util_encode.go
Normal file
@ -0,0 +1,30 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
//给密码字符串加密
|
||||
func GetMd5(raw string) string {
|
||||
return fmt.Sprintf("%x", md5.Sum([]byte(raw)))
|
||||
}
|
||||
|
||||
func GetBcrypt(raw string) string {
|
||||
|
||||
password := []byte(raw)
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(hashedPassword)
|
||||
}
|
||||
|
||||
func MatchBcrypt(raw string, bcryptStr string) bool {
|
||||
|
||||
err := bcrypt.CompareHashAndPassword([]byte(bcryptStr), []byte(raw))
|
||||
|
||||
return err == nil
|
||||
|
||||
}
|
17
code/tool/util_framework.go
Normal file
17
code/tool/util_framework.go
Normal file
@ -0,0 +1,17 @@
|
||||
package tool
|
||||
|
||||
|
||||
//带有panic恢复的方法
|
||||
func PanicHandler() {
|
||||
if err := recover(); err != nil {
|
||||
//TODO 全局日志记录
|
||||
//LOGGER.Error("异步任务错误: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
//带有panic恢复的方法
|
||||
func SafeMethod(f func()) {
|
||||
defer PanicHandler()
|
||||
//执行函数
|
||||
f()
|
||||
}
|
664
code/tool/util_mime.go
Normal file
664
code/tool/util_mime.go
Normal file
@ -0,0 +1,664 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var allMimeMap = map[string]string{
|
||||
".323": "text/h323",
|
||||
".3g2": "video/3gpp2",
|
||||
".3gp": "video/3gpp",
|
||||
".3gp2": "video/3gpp2",
|
||||
".3gpp": "video/3gpp",
|
||||
".7z": "application/x-7z-compressed",
|
||||
".aa": "audio/audible",
|
||||
".AAC": "audio/aac",
|
||||
".aaf": "application/octet-stream",
|
||||
".aax": "audio/vnd.audible.aax",
|
||||
".ac3": "audio/ac3",
|
||||
".aca": "application/octet-stream",
|
||||
".accda": "application/msaccess.addin",
|
||||
".accdb": "application/msaccess",
|
||||
".accdc": "application/msaccess.cab",
|
||||
".accde": "application/msaccess",
|
||||
".accdr": "application/msaccess.runtime",
|
||||
".accdt": "application/msaccess",
|
||||
".accdw": "application/msaccess.webapplication",
|
||||
".accft": "application/msaccess.ftemplate",
|
||||
".acx": "application/internet-property-stream",
|
||||
".AddIn": "text/xml",
|
||||
".ade": "application/msaccess",
|
||||
".adobebridge": "application/x-bridge-url",
|
||||
".adp": "application/msaccess",
|
||||
".ADT": "audio/vnd.dlna.adts",
|
||||
".ADTS": "audio/aac",
|
||||
".afm": "application/octet-stream",
|
||||
".ai": "application/postscript",
|
||||
".aif": "audio/aiff",
|
||||
".aifc": "audio/aiff",
|
||||
".aiff": "audio/aiff",
|
||||
".air": "application/vnd.adobe.air-application-installer-package+zip",
|
||||
".amc": "application/mpeg",
|
||||
".anx": "application/annodex",
|
||||
".apk": "application/vnd.android.package-archive",
|
||||
".application": "application/x-ms-application",
|
||||
".art": "image/x-jg",
|
||||
".asa": "application/xml",
|
||||
".asax": "application/xml",
|
||||
".ascx": "application/xml",
|
||||
".asd": "application/octet-stream",
|
||||
".asf": "video/x-ms-asf",
|
||||
".ashx": "application/xml",
|
||||
".asi": "application/octet-stream",
|
||||
".asm": "text/plain",
|
||||
".asmx": "application/xml",
|
||||
".aspx": "application/xml",
|
||||
".asr": "video/x-ms-asf",
|
||||
".asx": "video/x-ms-asf",
|
||||
".atom": "application/atom+xml",
|
||||
".au": "audio/basic",
|
||||
".avi": "video/x-msvideo",
|
||||
".axa": "audio/annodex",
|
||||
".axs": "application/olescript",
|
||||
".axv": "video/annodex",
|
||||
".bas": "text/plain",
|
||||
".bat": "text/plain",
|
||||
".bcpio": "application/x-bcpio",
|
||||
".bin": "application/octet-stream",
|
||||
".bmp": "image/bmp",
|
||||
".c": "text/plain",
|
||||
".cab": "application/octet-stream",
|
||||
".caf": "audio/x-caf",
|
||||
".calx": "application/vnd.ms-office.calx",
|
||||
".cat": "application/vnd.ms-pki.seccat",
|
||||
".cc": "text/plain",
|
||||
".cd": "text/plain",
|
||||
".cdda": "audio/aiff",
|
||||
".cdf": "application/x-cdf",
|
||||
".cer": "application/x-x509-ca-cert",
|
||||
".cfg": "text/plain",
|
||||
".chm": "application/octet-stream",
|
||||
".class": "application/x-java-applet",
|
||||
".clp": "application/x-msclip",
|
||||
".cmd": "text/plain",
|
||||
".cmx": "image/x-cmx",
|
||||
".cnf": "text/plain",
|
||||
".cod": "image/cis-cod",
|
||||
".config": "application/xml",
|
||||
".contact": "text/x-ms-contact",
|
||||
".coverage": "application/xml",
|
||||
".cpio": "application/x-cpio",
|
||||
".cpp": "text/plain",
|
||||
".crd": "application/x-mscardfile",
|
||||
".crl": "application/pkix-crl",
|
||||
".crt": "application/x-x509-ca-cert",
|
||||
".cs": "text/plain",
|
||||
".csdproj": "text/plain",
|
||||
".csh": "application/x-csh",
|
||||
".csproj": "text/plain",
|
||||
".css": "text/css",
|
||||
".csv": "application/csv",
|
||||
".cur": "application/octet-stream",
|
||||
".cxx": "text/plain",
|
||||
".dat": "application/octet-stream",
|
||||
".datasource": "application/xml",
|
||||
".dbproj": "text/plain",
|
||||
".dcr": "application/x-director",
|
||||
".def": "text/plain",
|
||||
".deploy": "application/octet-stream",
|
||||
".der": "application/x-x509-ca-cert",
|
||||
".dgml": "application/xml",
|
||||
".dib": "image/bmp",
|
||||
".dif": "video/x-dv",
|
||||
".dir": "application/x-director",
|
||||
".disco": "text/xml",
|
||||
".divx": "video/divx",
|
||||
".dll": "application/x-msdownload",
|
||||
".dll.config": "text/xml",
|
||||
".dlm": "text/dlm",
|
||||
".doc": "application/msword",
|
||||
".docm": "application/vnd.ms-word.document.macroEnabled.12",
|
||||
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
".dot": "application/msword",
|
||||
".dotm": "application/vnd.ms-word.template.macroEnabled.12",
|
||||
".dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
|
||||
".dsp": "application/octet-stream",
|
||||
".dsw": "text/plain",
|
||||
".dtd": "text/xml",
|
||||
".dtsConfig": "text/xml",
|
||||
".dv": "video/x-dv",
|
||||
".dvi": "application/x-dvi",
|
||||
".dwf": "drawing/x-dwf",
|
||||
".dwg": "application/acad",
|
||||
".dwp": "application/octet-stream",
|
||||
".dxf": "application/x-dxf",
|
||||
".dxr": "application/x-director",
|
||||
".eml": "message/rfc822",
|
||||
".emz": "application/octet-stream",
|
||||
".eot": "application/vnd.ms-fontobject",
|
||||
".eps": "application/postscript",
|
||||
".etl": "application/etl",
|
||||
".etx": "text/x-setext",
|
||||
".evy": "application/envoy",
|
||||
".exe": "application/octet-stream",
|
||||
".exe.config": "text/xml",
|
||||
".fdf": "application/vnd.fdf",
|
||||
".fif": "application/fractals",
|
||||
".filters": "application/xml",
|
||||
".fla": "application/octet-stream",
|
||||
".flac": "audio/flac",
|
||||
".flr": "x-world/x-vrml",
|
||||
".flv": "video/x-flv",
|
||||
".fsscript": "application/fsharp-script",
|
||||
".fsx": "application/fsharp-script",
|
||||
".generictest": "application/xml",
|
||||
".gif": "image/gif",
|
||||
".go": "text/plain",
|
||||
".gpx": "application/gpx+xml",
|
||||
".group": "text/x-ms-group",
|
||||
".gsm": "audio/x-gsm",
|
||||
".gradle": "text/plain",
|
||||
".gtar": "application/x-gtar",
|
||||
".gz": "application/x-gzip",
|
||||
".h": "text/plain",
|
||||
".hdf": "application/x-hdf",
|
||||
".hdml": "text/x-hdml",
|
||||
".hhc": "application/x-oleobject",
|
||||
".hhk": "application/octet-stream",
|
||||
".hhp": "application/octet-stream",
|
||||
".hlp": "application/winhlp",
|
||||
".hpp": "text/plain",
|
||||
".hqx": "application/mac-binhex40",
|
||||
".hta": "application/hta",
|
||||
".htc": "text/x-component",
|
||||
".htm": "text/html",
|
||||
".html": "text/html",
|
||||
".htt": "text/webviewhtml",
|
||||
".hxa": "application/xml",
|
||||
".hxc": "application/xml",
|
||||
".hxd": "application/octet-stream",
|
||||
".hxe": "application/xml",
|
||||
".hxf": "application/xml",
|
||||
".hxh": "application/octet-stream",
|
||||
".hxi": "application/octet-stream",
|
||||
".hxk": "application/xml",
|
||||
".hxq": "application/octet-stream",
|
||||
".hxr": "application/octet-stream",
|
||||
".hxs": "application/octet-stream",
|
||||
".hxt": "text/html",
|
||||
".hxv": "application/xml",
|
||||
".hxw": "application/octet-stream",
|
||||
".hxx": "text/plain",
|
||||
".i": "text/plain",
|
||||
".ico": "image/x-icon",
|
||||
".ics": "application/octet-stream",
|
||||
".idl": "text/plain",
|
||||
".ief": "image/ief",
|
||||
".iii": "application/x-iphone",
|
||||
".inc": "text/plain",
|
||||
".inf": "application/octet-stream",
|
||||
".ini": "text/plain",
|
||||
".inl": "text/plain",
|
||||
".ins": "application/x-internet-signup",
|
||||
".ipa": "application/x-itunes-ipa",
|
||||
".ipg": "application/x-itunes-ipg",
|
||||
".ipproj": "text/plain",
|
||||
".ipsw": "application/x-itunes-ipsw",
|
||||
".iqy": "text/x-ms-iqy",
|
||||
".isp": "application/x-internet-signup",
|
||||
".ite": "application/x-itunes-ite",
|
||||
".itlp": "application/x-itunes-itlp",
|
||||
".itms": "application/x-itunes-itms",
|
||||
".itpc": "application/x-itunes-itpc",
|
||||
".IVF": "video/x-ivf",
|
||||
".jar": "application/java-archive",
|
||||
".java": "text/plain",
|
||||
".jck": "application/liquidmotion",
|
||||
".jcz": "application/liquidmotion",
|
||||
".jfif": "image/pjpeg",
|
||||
".jnlp": "application/x-java-jnlp-file",
|
||||
".jpb": "application/octet-stream",
|
||||
".jpe": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".jpg": "image/jpeg",
|
||||
".js": "application/javascript",
|
||||
".json": "application/json",
|
||||
".jsx": "text/jscript",
|
||||
".jsxbin": "text/plain",
|
||||
".latex": "application/x-latex",
|
||||
".library-ms": "application/windows-library+xml",
|
||||
".lit": "application/x-ms-reader",
|
||||
".loadtest": "application/xml",
|
||||
".lpk": "application/octet-stream",
|
||||
".lsf": "video/x-la-asf",
|
||||
".lst": "text/plain",
|
||||
".lsx": "video/x-la-asf",
|
||||
".lzh": "application/octet-stream",
|
||||
".m13": "application/x-msmediaview",
|
||||
".m14": "application/x-msmediaview",
|
||||
".m1v": "video/mpeg",
|
||||
".m2t": "video/vnd.dlna.mpeg-tts",
|
||||
".m2ts": "video/vnd.dlna.mpeg-tts",
|
||||
".m2v": "video/mpeg",
|
||||
".m3u": "audio/x-mpegurl",
|
||||
".m3u8": "audio/x-mpegurl",
|
||||
".m4a": "audio/m4a",
|
||||
".m4b": "audio/m4b",
|
||||
".m4p": "audio/m4p",
|
||||
".m4r": "audio/x-m4r",
|
||||
".m4v": "video/x-m4v",
|
||||
".mac": "image/x-macpaint",
|
||||
".mak": "text/plain",
|
||||
".man": "application/x-troff-man",
|
||||
".manifest": "application/x-ms-manifest",
|
||||
".map": "text/plain",
|
||||
".master": "application/xml",
|
||||
".mbox": "application/mbox",
|
||||
".mda": "application/msaccess",
|
||||
".mdb": "application/x-msaccess",
|
||||
".mde": "application/msaccess",
|
||||
".mdp": "application/octet-stream",
|
||||
".me": "application/x-troff-me",
|
||||
".mfp": "application/x-shockwave-flash",
|
||||
".mht": "message/rfc822",
|
||||
".mhtml": "message/rfc822",
|
||||
".mid": "audio/mid",
|
||||
".midi": "audio/mid",
|
||||
".mix": "application/octet-stream",
|
||||
".mk": "text/plain",
|
||||
".mmf": "application/x-smaf",
|
||||
".mno": "text/xml",
|
||||
".mny": "application/x-msmoney",
|
||||
".mod": "video/mpeg",
|
||||
".mov": "video/quicktime",
|
||||
".movie": "video/x-sgi-movie",
|
||||
".mp2": "video/mpeg",
|
||||
".mp2v": "video/mpeg",
|
||||
".mp3": "audio/mpeg",
|
||||
".mp4": "video/mp4",
|
||||
".mp4v": "video/mp4",
|
||||
".mpa": "video/mpeg",
|
||||
".mpe": "video/mpeg",
|
||||
".mpeg": "video/mpeg",
|
||||
".mpf": "application/vnd.ms-mediapackage",
|
||||
".mpg": "video/mpeg",
|
||||
".mpp": "application/vnd.ms-project",
|
||||
".mpv2": "video/mpeg",
|
||||
".mqv": "video/quicktime",
|
||||
".ms": "application/x-troff-ms",
|
||||
".msg": "application/vnd.ms-outlook",
|
||||
".msi": "application/octet-stream",
|
||||
".mso": "application/octet-stream",
|
||||
".mts": "video/vnd.dlna.mpeg-tts",
|
||||
".mtx": "application/xml",
|
||||
".mvb": "application/x-msmediaview",
|
||||
".mvc": "application/x-miva-compiled",
|
||||
".mxp": "application/x-mmxp",
|
||||
".nc": "application/x-netcdf",
|
||||
".nsc": "video/x-ms-asf",
|
||||
".nws": "message/rfc822",
|
||||
".ocx": "application/octet-stream",
|
||||
".oda": "application/oda",
|
||||
".odb": "application/vnd.oasis.opendocument.database",
|
||||
".odc": "application/vnd.oasis.opendocument.chart",
|
||||
".odf": "application/vnd.oasis.opendocument.formula",
|
||||
".odg": "application/vnd.oasis.opendocument.graphics",
|
||||
".odh": "text/plain",
|
||||
".odi": "application/vnd.oasis.opendocument.image",
|
||||
".odl": "text/plain",
|
||||
".odm": "application/vnd.oasis.opendocument.text-master",
|
||||
".odp": "application/vnd.oasis.opendocument.presentation",
|
||||
".ods": "application/vnd.oasis.opendocument.spreadsheet",
|
||||
".odt": "application/vnd.oasis.opendocument.text",
|
||||
".oga": "audio/ogg",
|
||||
".ogg": "audio/ogg",
|
||||
".ogv": "video/ogg",
|
||||
".ogx": "application/ogg",
|
||||
".one": "application/onenote",
|
||||
".onea": "application/onenote",
|
||||
".onepkg": "application/onenote",
|
||||
".onetmp": "application/onenote",
|
||||
".onetoc": "application/onenote",
|
||||
".onetoc2": "application/onenote",
|
||||
".opus": "audio/ogg",
|
||||
".orderedtest": "application/xml",
|
||||
".osdx": "application/opensearchdescription+xml",
|
||||
".otf": "application/font-sfnt",
|
||||
".otg": "application/vnd.oasis.opendocument.graphics-template",
|
||||
".oth": "application/vnd.oasis.opendocument.text-web",
|
||||
".otp": "application/vnd.oasis.opendocument.presentation-template",
|
||||
".ots": "application/vnd.oasis.opendocument.spreadsheet-template",
|
||||
".ott": "application/vnd.oasis.opendocument.text-template",
|
||||
".oxt": "application/vnd.openofficeorg.extension",
|
||||
".p10": "application/pkcs10",
|
||||
".p12": "application/x-pkcs12",
|
||||
".p7b": "application/x-pkcs7-certificates",
|
||||
".p7c": "application/pkcs7-mime",
|
||||
".p7m": "application/pkcs7-mime",
|
||||
".p7r": "application/x-pkcs7-certreqresp",
|
||||
".p7s": "application/pkcs7-signature",
|
||||
".pbm": "image/x-portable-bitmap",
|
||||
".pcast": "application/x-podcast",
|
||||
".pct": "image/pict",
|
||||
".pcx": "application/octet-stream",
|
||||
".pcz": "application/octet-stream",
|
||||
".pdf": "application/pdf",
|
||||
".pfb": "application/octet-stream",
|
||||
".pfm": "application/octet-stream",
|
||||
".pfx": "application/x-pkcs12",
|
||||
".pgm": "image/x-portable-graymap",
|
||||
".pic": "image/pict",
|
||||
".pict": "image/pict",
|
||||
".pkgdef": "text/plain",
|
||||
".pkgundef": "text/plain",
|
||||
".pko": "application/vnd.ms-pki.pko",
|
||||
".pls": "audio/scpls",
|
||||
".pma": "application/x-perfmon",
|
||||
".pmc": "application/x-perfmon",
|
||||
".pml": "application/x-perfmon",
|
||||
".pmr": "application/x-perfmon",
|
||||
".pmw": "application/x-perfmon",
|
||||
".png": "image/png",
|
||||
".pnm": "image/x-portable-anymap",
|
||||
".pnt": "image/x-macpaint",
|
||||
".pntg": "image/x-macpaint",
|
||||
".pnz": "image/png",
|
||||
".pot": "application/vnd.ms-powerpoint",
|
||||
".potm": "application/vnd.ms-powerpoint.template.macroEnabled.12",
|
||||
".potx": "application/vnd.openxmlformats-officedocument.presentationml.template",
|
||||
".ppa": "application/vnd.ms-powerpoint",
|
||||
".ppam": "application/vnd.ms-powerpoint.addin.macroEnabled.12",
|
||||
".ppm": "image/x-portable-pixmap",
|
||||
".pps": "application/vnd.ms-powerpoint",
|
||||
".ppsm": "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
|
||||
".ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
|
||||
".ppt": "application/vnd.ms-powerpoint",
|
||||
".pptm": "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
|
||||
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
".prf": "application/pics-rules",
|
||||
".prm": "application/octet-stream",
|
||||
".prx": "application/octet-stream",
|
||||
".ps": "application/postscript",
|
||||
".py": "text/plain",
|
||||
".psc1": "application/PowerShell",
|
||||
".psd": "application/octet-stream",
|
||||
".psess": "application/xml",
|
||||
".psm": "application/octet-stream",
|
||||
".psp": "application/octet-stream",
|
||||
".pst": "application/vnd.ms-outlook",
|
||||
".pub": "application/x-mspublisher",
|
||||
".pwz": "application/vnd.ms-powerpoint",
|
||||
".qht": "text/x-html-insertion",
|
||||
".qhtm": "text/x-html-insertion",
|
||||
".qt": "video/quicktime",
|
||||
".qti": "image/x-quicktime",
|
||||
".qtif": "image/x-quicktime",
|
||||
".qtl": "application/x-quicktimeplayer",
|
||||
".qxd": "application/octet-stream",
|
||||
".ra": "audio/x-pn-realaudio",
|
||||
".ram": "audio/x-pn-realaudio",
|
||||
".rar": "application/x-rar-compressed",
|
||||
".ras": "image/x-cmu-raster",
|
||||
".rat": "application/rat-file",
|
||||
".rc": "text/plain",
|
||||
".rc2": "text/plain",
|
||||
".rct": "text/plain",
|
||||
".rdlc": "application/xml",
|
||||
".reg": "text/plain",
|
||||
".resx": "application/xml",
|
||||
".rf": "image/vnd.rn-realflash",
|
||||
".rgb": "image/x-rgb",
|
||||
".rgs": "text/plain",
|
||||
".rm": "application/vnd.rn-realmedia",
|
||||
".rmi": "audio/mid",
|
||||
".rmp": "application/vnd.rn-rn_music_package",
|
||||
".roff": "application/x-troff",
|
||||
".rpm": "audio/x-pn-realaudio-plugin",
|
||||
".rqy": "text/x-ms-rqy",
|
||||
".rtf": "application/rtf",
|
||||
".rtx": "text/richtext",
|
||||
".rvt": "application/octet-stream",
|
||||
".ruleset": "application/xml",
|
||||
".s": "text/plain",
|
||||
".safariextz": "application/x-safari-safariextz",
|
||||
".scd": "application/x-msschedule",
|
||||
".scr": "text/plain",
|
||||
".sct": "text/scriptlet",
|
||||
".sd2": "audio/x-sd2",
|
||||
".sdp": "application/sdp",
|
||||
".sea": "application/octet-stream",
|
||||
".searchConnector-ms": "application/windows-search-connector+xml",
|
||||
".setpay": "application/set-payment-initiation",
|
||||
".setreg": "application/set-registration-initiation",
|
||||
".settings": "application/xml",
|
||||
".sgimb": "application/x-sgimb",
|
||||
".sgml": "text/sgml",
|
||||
".sh": "text/plain",
|
||||
".shar": "application/x-shar",
|
||||
".shtml": "text/html",
|
||||
".sit": "application/x-stuffit",
|
||||
".sitemap": "application/xml",
|
||||
".skin": "application/xml",
|
||||
".skp": "application/x-koan",
|
||||
".sldm": "application/vnd.ms-powerpoint.slide.macroEnabled.12",
|
||||
".sldx": "application/vnd.openxmlformats-officedocument.presentationml.slide",
|
||||
".slk": "application/vnd.ms-excel",
|
||||
".sln": "text/plain",
|
||||
".slupkg-ms": "application/x-ms-license",
|
||||
".smd": "audio/x-smd",
|
||||
".smi": "application/octet-stream",
|
||||
".smx": "audio/x-smd",
|
||||
".smz": "audio/x-smd",
|
||||
".snd": "audio/basic",
|
||||
".snippet": "application/xml",
|
||||
".snp": "application/octet-stream",
|
||||
".sol": "text/plain",
|
||||
".sor": "text/plain",
|
||||
".spc": "application/x-pkcs7-certificates",
|
||||
".spl": "application/futuresplash",
|
||||
".spx": "audio/ogg",
|
||||
".sql": "text/plain",
|
||||
".src": "application/x-wais-source",
|
||||
".srf": "text/plain",
|
||||
".SSISDeploymentManifest": "text/xml",
|
||||
".ssm": "application/streamingmedia",
|
||||
".sst": "application/vnd.ms-pki.certstore",
|
||||
".stl": "application/vnd.ms-pki.stl",
|
||||
".sv4cpio": "application/x-sv4cpio",
|
||||
".sv4crc": "application/x-sv4crc",
|
||||
".svc": "application/xml",
|
||||
".svg": "image/svg+xml",
|
||||
".swf": "application/x-shockwave-flash",
|
||||
".step": "application/step",
|
||||
".stp": "application/step",
|
||||
".t": "application/x-troff",
|
||||
".tar": "application/x-tar",
|
||||
".tcl": "application/x-tcl",
|
||||
".testrunconfig": "application/xml",
|
||||
".testsettings": "application/xml",
|
||||
".tex": "application/x-tex",
|
||||
".texi": "application/x-texinfo",
|
||||
".texinfo": "application/x-texinfo",
|
||||
".tgz": "application/x-compressed",
|
||||
".thmx": "application/vnd.ms-officetheme",
|
||||
".thn": "application/octet-stream",
|
||||
".tif": "image/tiff",
|
||||
".tiff": "image/tiff",
|
||||
".tlh": "text/plain",
|
||||
".tli": "text/plain",
|
||||
".toc": "application/octet-stream",
|
||||
".tr": "application/x-troff",
|
||||
".trm": "application/x-msterminal",
|
||||
".trx": "application/xml",
|
||||
".ts": "video/vnd.dlna.mpeg-tts",
|
||||
".tsv": "text/tab-separated-values",
|
||||
".ttf": "application/font-sfnt",
|
||||
".tts": "video/vnd.dlna.mpeg-tts",
|
||||
".txt": "text/plain",
|
||||
".u32": "application/octet-stream",
|
||||
".uls": "text/iuls",
|
||||
".user": "text/plain",
|
||||
".ustar": "application/x-ustar",
|
||||
".vb": "text/plain",
|
||||
".vbdproj": "text/plain",
|
||||
".vbk": "video/mpeg",
|
||||
".vbproj": "text/plain",
|
||||
".vbs": "text/vbscript",
|
||||
".vcf": "text/x-vcard",
|
||||
".vcproj": "application/xml",
|
||||
".vcs": "text/plain",
|
||||
".vcxproj": "application/xml",
|
||||
".vddproj": "text/plain",
|
||||
".vdp": "text/plain",
|
||||
".vdproj": "text/plain",
|
||||
".vdx": "application/vnd.ms-visio.viewer",
|
||||
".vml": "text/xml",
|
||||
".vscontent": "application/xml",
|
||||
".vsct": "text/xml",
|
||||
".vsd": "application/vnd.visio",
|
||||
".vsi": "application/ms-vsi",
|
||||
".vsix": "application/vsix",
|
||||
".vsixlangpack": "text/xml",
|
||||
".vsixmanifest": "text/xml",
|
||||
".vsmdi": "application/xml",
|
||||
".vspscc": "text/plain",
|
||||
".vss": "application/vnd.visio",
|
||||
".vsscc": "text/plain",
|
||||
".vssettings": "text/xml",
|
||||
".vssscc": "text/plain",
|
||||
".vst": "application/vnd.visio",
|
||||
".vstemplate": "text/xml",
|
||||
".vsto": "application/x-ms-vsto",
|
||||
".vsw": "application/vnd.visio",
|
||||
".vsx": "application/vnd.visio",
|
||||
".vtx": "application/vnd.visio",
|
||||
".wav": "audio/wav",
|
||||
".wave": "audio/wav",
|
||||
".wax": "audio/x-ms-wax",
|
||||
".wbk": "application/msword",
|
||||
".wbmp": "image/vnd.wap.wbmp",
|
||||
".wcm": "application/vnd.ms-works",
|
||||
".wdb": "application/vnd.ms-works",
|
||||
".wdp": "image/vnd.ms-photo",
|
||||
".webarchive": "application/x-safari-webarchive",
|
||||
".webm": "video/webm",
|
||||
".webp": "image/webp", /* https"://en.wikipedia.org/wiki/WebP */
|
||||
".webtest": "application/xml",
|
||||
".wiq": "application/xml",
|
||||
".wiz": "application/msword",
|
||||
".wks": "application/vnd.ms-works",
|
||||
".WLMP": "application/wlmoviemaker",
|
||||
".wlpginstall": "application/x-wlpg-detect",
|
||||
".wlpginstall3": "application/x-wlpg3-detect",
|
||||
".wm": "video/x-ms-wm",
|
||||
".wma": "audio/x-ms-wma",
|
||||
".wmd": "application/x-ms-wmd",
|
||||
".wmf": "application/x-msmetafile",
|
||||
".wml": "text/vnd.wap.wml",
|
||||
".wmlc": "application/vnd.wap.wmlc",
|
||||
".wmls": "text/vnd.wap.wmlscript",
|
||||
".wmlsc": "application/vnd.wap.wmlscriptc",
|
||||
".wmp": "video/x-ms-wmp",
|
||||
".wmv": "video/x-ms-wmv",
|
||||
".wmx": "video/x-ms-wmx",
|
||||
".wmz": "application/x-ms-wmz",
|
||||
".woff": "application/font-woff",
|
||||
".wpl": "application/vnd.ms-wpl",
|
||||
".wps": "application/vnd.ms-works",
|
||||
".wri": "application/x-mswrite",
|
||||
".wrl": "x-world/x-vrml",
|
||||
".wrz": "x-world/x-vrml",
|
||||
".wsc": "text/scriptlet",
|
||||
".wsdl": "text/xml",
|
||||
".wvx": "video/x-ms-wvx",
|
||||
".x": "application/directx",
|
||||
".xaf": "x-world/x-vrml",
|
||||
".xaml": "application/xaml+xml",
|
||||
".xap": "application/x-silverlight-app",
|
||||
".xbap": "application/x-ms-xbap",
|
||||
".xbm": "image/x-xbitmap",
|
||||
".xdr": "text/plain",
|
||||
".xht": "application/xhtml+xml",
|
||||
".xhtml": "application/xhtml+xml",
|
||||
".xla": "application/vnd.ms-excel",
|
||||
".xlam": "application/vnd.ms-excel.addin.macroEnabled.12",
|
||||
".xlc": "application/vnd.ms-excel",
|
||||
".xld": "application/vnd.ms-excel",
|
||||
".xlk": "application/vnd.ms-excel",
|
||||
".xll": "application/vnd.ms-excel",
|
||||
".xlm": "application/vnd.ms-excel",
|
||||
".xls": "application/vnd.ms-excel",
|
||||
".xlsb": "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
|
||||
".xlsm": "application/vnd.ms-excel.sheet.macroEnabled.12",
|
||||
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
".xlt": "application/vnd.ms-excel",
|
||||
".xltm": "application/vnd.ms-excel.template.macroEnabled.12",
|
||||
".xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
|
||||
".xlw": "application/vnd.ms-excel",
|
||||
".xml": "text/xml",
|
||||
".xmp": "application/octet-stream",
|
||||
".xmta": "application/xml",
|
||||
".xof": "x-world/x-vrml",
|
||||
".XOML": "text/plain",
|
||||
".xpm": "image/x-xpixmap",
|
||||
".xps": "application/vnd.ms-xpsdocument",
|
||||
".xrm-ms": "text/xml",
|
||||
".xsc": "application/xml",
|
||||
".xsd": "text/xml",
|
||||
".xsf": "text/xml",
|
||||
".xsl": "text/xml",
|
||||
".xslt": "text/xml",
|
||||
".xsn": "application/octet-stream",
|
||||
".xss": "application/xml",
|
||||
".xspf": "application/xspf+xml",
|
||||
".xtp": "application/octet-stream",
|
||||
".xwd": "image/x-xwindowdump",
|
||||
".z": "application/x-compress",
|
||||
".zip": "application/zip"}
|
||||
|
||||
//根据文件名字获取后缀名,均是小写。
|
||||
func GetExtension(filename string) string {
|
||||
|
||||
var extension = filepath.Ext(filename)
|
||||
|
||||
return strings.ToLower(extension)
|
||||
|
||||
}
|
||||
|
||||
//根据文件名字获取去除后缀的名称
|
||||
func GetSimpleFileName(filename string) string {
|
||||
|
||||
for i := len(filename) - 1; i >= 0 && !os.IsPathSeparator(filename[i]); i-- {
|
||||
if filename[i] == '.' {
|
||||
return filename[:i]
|
||||
}
|
||||
}
|
||||
return filename
|
||||
|
||||
}
|
||||
|
||||
//根据一个后缀名获取MimeType,获取不到默认返回 "application/octet-stream"
|
||||
func GetMimeType(filename string) string {
|
||||
|
||||
extension := GetExtension(filename)
|
||||
|
||||
if mimeType, ok := allMimeMap[extension]; ok {
|
||||
return mimeType
|
||||
} else {
|
||||
return "application/octet-stream"
|
||||
}
|
||||
}
|
||||
|
||||
//根据一个后缀名获取MimeType,获取不到默认返回fallback.
|
||||
func GetFallbackMimeType(filename string, fallback string) string {
|
||||
|
||||
extension := GetExtension(filename)
|
||||
|
||||
if mimeType, ok := allMimeMap[extension]; ok {
|
||||
return mimeType
|
||||
} else {
|
||||
return fallback
|
||||
}
|
||||
}
|
66
code/tool/util_network.go
Normal file
66
code/tool/util_network.go
Normal file
@ -0,0 +1,66 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//根据一个请求,获取ip.
|
||||
func GetIpAddress(r *http.Request) string {
|
||||
var ipAddress string
|
||||
|
||||
ipAddress = r.RemoteAddr
|
||||
|
||||
if ipAddress != "" {
|
||||
ipAddress = strings.Split(ipAddress, ":")[0]
|
||||
}
|
||||
|
||||
for _, h := range []string{"X-Forwarded-For", "X-Real-Ip"} {
|
||||
for _, ip := range strings.Split(r.Header.Get(h), ",") {
|
||||
if ip != "" {
|
||||
ipAddress = ip
|
||||
}
|
||||
}
|
||||
}
|
||||
return ipAddress
|
||||
}
|
||||
|
||||
//根据一个请求,获取host
|
||||
func GetHostFromRequest(r *http.Request) string {
|
||||
|
||||
return r.Host
|
||||
|
||||
}
|
||||
|
||||
//根据一个请求,获取authenticationId
|
||||
func GetSessionUuidFromRequest(request *http.Request, cookieAuthKey string) string {
|
||||
|
||||
//验证用户是否已经登录。
|
||||
sessionCookie, err := request.Cookie(cookieAuthKey)
|
||||
var sessionId string
|
||||
if err != nil {
|
||||
//从入参中捞取
|
||||
sessionId = request.FormValue(cookieAuthKey)
|
||||
} else {
|
||||
sessionId = sessionCookie.Value
|
||||
}
|
||||
|
||||
return sessionId
|
||||
|
||||
}
|
||||
|
||||
//允许跨域请求
|
||||
func AllowCORS(writer http.ResponseWriter) {
|
||||
writer.Header().Add("Access-Control-Allow-Origin", "*")
|
||||
writer.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
|
||||
writer.Header().Add("Access-Control-Max-Age", "3600")
|
||||
writer.Header().Add("Access-Control-Allow-Headers", "content-type")
|
||||
}
|
||||
|
||||
//禁用缓存
|
||||
func DisableCache(writer http.ResponseWriter) {
|
||||
//对于IE浏览器,会自动缓存,因此设置不缓存Header.
|
||||
writer.Header().Set("Pragma", "No-cache")
|
||||
writer.Header().Set("Cache-Control", "no-cache")
|
||||
writer.Header().Set("Expires", "0")
|
||||
}
|
253
code/tool/util_path.go
Normal file
253
code/tool/util_path.go
Normal file
@ -0,0 +1,253 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/build"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"tank/code/result"
|
||||
)
|
||||
|
||||
//判断文件或文件夹是否已经存在
|
||||
func PathExists(path string) (bool, error) {
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
//获取GOPATH路径
|
||||
func GetGoPath() string {
|
||||
|
||||
return build.Default.GOPATH
|
||||
|
||||
}
|
||||
|
||||
//获取该应用可执行文件的位置。
|
||||
//例如:C:\Users\lishuang\AppData\Local\Temp
|
||||
func GetHomePath() string {
|
||||
ex, err := os.Executable()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
exPath := filepath.Dir(ex)
|
||||
|
||||
//如果exPath中包含了 /private/var/folders 我们认为是在Mac的开发环境中
|
||||
macDev := strings.HasPrefix(exPath, "/private/var/folders")
|
||||
if macDev {
|
||||
exPath = GetGoPath() + "/src/tank/tmp"
|
||||
}
|
||||
|
||||
//如果exPath中包含了 \\AppData\\Local\\Temp 我们认为是在Win的开发环境中
|
||||
systemUser, err := user.Current()
|
||||
winDev := strings.HasPrefix(exPath, systemUser.HomeDir+"\\AppData\\Local\\Temp")
|
||||
if winDev {
|
||||
exPath = GetGoPath() + "/src/tank/tmp"
|
||||
}
|
||||
|
||||
return exPath
|
||||
}
|
||||
|
||||
//获取前端静态资源的位置。如果你在开发模式下,可以将这里直接返回tank/build下面的html路径。
|
||||
//例如:C:/Users/lishuang/AppData/Local/Temp/html
|
||||
func GetHtmlPath() string {
|
||||
|
||||
homePath := GetHomePath()
|
||||
filePath := homePath + "/html"
|
||||
exists, err := PathExists(filePath)
|
||||
if err != nil {
|
||||
panic("判断上传文件是否存在时出错!")
|
||||
}
|
||||
if !exists {
|
||||
err = os.MkdirAll(filePath, 0777)
|
||||
if err != nil {
|
||||
panic("创建上传文件夹时出错!")
|
||||
}
|
||||
}
|
||||
|
||||
return filePath
|
||||
}
|
||||
|
||||
//如果文件夹存在就不管,不存在就创建。 例如:/var/www/matter
|
||||
func MakeDirAll(dirPath string) string {
|
||||
|
||||
exists, err := PathExists(dirPath)
|
||||
if err != nil {
|
||||
panic("判断文件是否存在时出错!")
|
||||
}
|
||||
if !exists {
|
||||
//TODO:文件权限需要进一步考虑
|
||||
err = os.MkdirAll(dirPath, 0777)
|
||||
if err != nil {
|
||||
panic("创建文件夹时出错!")
|
||||
}
|
||||
}
|
||||
|
||||
return dirPath
|
||||
}
|
||||
|
||||
//获取到一个Path的文件夹路径,eg /var/www/xx.log -> /var/www
|
||||
func GetDirOfPath(fullPath string) string {
|
||||
|
||||
index1 := strings.LastIndex(fullPath, "/")
|
||||
//可能是windows的环境
|
||||
index2 := strings.LastIndex(fullPath, "\\")
|
||||
index := index1
|
||||
if index2 > index1 {
|
||||
index = index2
|
||||
}
|
||||
|
||||
return fullPath[:index]
|
||||
}
|
||||
|
||||
//获取到一个Path 中的文件名,eg /var/www/xx.log -> xx.log
|
||||
func GetFilenameOfPath(fullPath string) string {
|
||||
|
||||
index1 := strings.LastIndex(fullPath, "/")
|
||||
//可能是windows的环境
|
||||
index2 := strings.LastIndex(fullPath, "\\")
|
||||
index := index1
|
||||
if index2 > index1 {
|
||||
index = index2
|
||||
}
|
||||
|
||||
return fullPath[index+1:]
|
||||
}
|
||||
|
||||
//尝试删除空文件夹 true表示删掉了一个空文件夹,false表示没有删掉任何东西
|
||||
func DeleteEmptyDir(dirPath string) bool {
|
||||
dir, err := ioutil.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
panic(result.BadRequest("尝试读取目录%s时出错 %s", dirPath, err.Error()))
|
||||
}
|
||||
if len(dir) == 0 {
|
||||
//空文件夹
|
||||
err = os.Remove(dirPath)
|
||||
if err != nil {
|
||||
panic(result.BadRequest("删除磁盘上的文件夹%s出错 %s", dirPath, err.Error()))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
//递归尝试删除空文件夹,一直空就一直删,直到不空为止
|
||||
func DeleteEmptyDirRecursive(dirPath string) {
|
||||
|
||||
fmt.Printf("递归删除删 %v \n", dirPath)
|
||||
|
||||
tmpPath := dirPath
|
||||
for DeleteEmptyDir(tmpPath) {
|
||||
|
||||
dir := GetDirOfPath(tmpPath)
|
||||
|
||||
fmt.Printf("尝试删除 %v\n", dir)
|
||||
|
||||
tmpPath = dir
|
||||
}
|
||||
}
|
||||
|
||||
//移除某个文件夹。 例如:/var/www/matter => /var/www
|
||||
func RemoveDirectory(dirPath string) string {
|
||||
|
||||
exists, err := PathExists(dirPath)
|
||||
if err != nil {
|
||||
panic("判断文件是否存在时出错!")
|
||||
}
|
||||
if exists {
|
||||
|
||||
err = os.Remove(dirPath)
|
||||
if err != nil {
|
||||
panic("删除文件夹时出错!")
|
||||
}
|
||||
}
|
||||
|
||||
return dirPath
|
||||
}
|
||||
|
||||
//获取配置文件存放的位置
|
||||
//例如:C:\Users\lishuang\AppData\Local\Temp/conf
|
||||
func GetConfPath() string {
|
||||
|
||||
homePath := GetHomePath()
|
||||
filePath := homePath + "/conf"
|
||||
exists, err := PathExists(filePath)
|
||||
if err != nil {
|
||||
panic("判断日志文件夹是否存在时出错!")
|
||||
}
|
||||
if !exists {
|
||||
err = os.MkdirAll(filePath, 0777)
|
||||
if err != nil {
|
||||
panic("创建日志文件夹时出错!")
|
||||
}
|
||||
}
|
||||
|
||||
return filePath
|
||||
}
|
||||
|
||||
//获取日志的路径
|
||||
//例如:默认存放于 home/log
|
||||
func GetLogPath() string {
|
||||
|
||||
homePath := GetHomePath()
|
||||
filePath := homePath + "/log"
|
||||
exists, err := PathExists(filePath)
|
||||
if err != nil {
|
||||
panic("判断日志文件夹是否存在时出错!")
|
||||
}
|
||||
if !exists {
|
||||
err = os.MkdirAll(filePath, 0777)
|
||||
if err != nil {
|
||||
panic("创建日志文件夹时出错!")
|
||||
}
|
||||
}
|
||||
|
||||
return filePath
|
||||
}
|
||||
|
||||
//复制文件
|
||||
func CopyFile(srcPath string, destPath string) (nBytes int64) {
|
||||
|
||||
srcFileStat, err := os.Stat(srcPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if !srcFileStat.Mode().IsRegular() {
|
||||
panic(fmt.Errorf("%s is not a regular file", srcPath))
|
||||
}
|
||||
|
||||
srcFile, err := os.Open(srcPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer func() {
|
||||
err = srcFile.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
destFile, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer func() {
|
||||
err = destFile.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
nBytes, err = io.Copy(destFile, srcFile)
|
||||
return nBytes
|
||||
}
|
42
code/tool/util_string.go
Normal file
42
code/tool/util_string.go
Normal file
@ -0,0 +1,42 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
//把一个大小转变成方便读的格式
|
||||
//human readable file size
|
||||
func HumanFileSize(bytes int64) string {
|
||||
var thresh int64 = 1024
|
||||
|
||||
if bytes < 0 {
|
||||
bytes = 0
|
||||
}
|
||||
if bytes < thresh {
|
||||
return fmt.Sprintf("%dB", bytes)
|
||||
}
|
||||
var units = []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
|
||||
|
||||
var u = 0
|
||||
var tmp = float64(bytes)
|
||||
var standard = float64(thresh)
|
||||
for tmp >= standard && u < len(units)-1 {
|
||||
tmp /= float64(standard)
|
||||
u++
|
||||
}
|
||||
|
||||
numStr := strconv.FormatFloat(tmp, 'f', 1, 64)
|
||||
|
||||
return fmt.Sprintf("%s%s", numStr, units[u])
|
||||
}
|
||||
|
||||
//获取MySQL的URL
|
||||
func GetMysqlUrl(
|
||||
mysqlPort int,
|
||||
mysqlHost string,
|
||||
mysqlSchema string,
|
||||
mysqlUsername string,
|
||||
mysqlPassword string) string {
|
||||
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local", mysqlUsername, mysqlPassword, mysqlHost, mysqlPort, mysqlSchema)
|
||||
}
|
63
code/tool/util_time.go
Normal file
63
code/tool/util_time.go
Normal file
@ -0,0 +1,63 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
//将一个时间字符串转换成时间对象(yyyy-MM-dd HH:mm:ss)
|
||||
func ConvertDateTimeStringToTime(timeString string) time.Time {
|
||||
local, _ := time.LoadLocation("Local")
|
||||
t, err := time.ParseInLocation("2006-01-02 15:04:05", timeString, local)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("不能将%s转为时间类型", timeString))
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
//将一个时间字符串转换成日期时间对象(yyyy-MM-dd HH:mm:ss)
|
||||
func ConvertTimeToDateTimeString(time time.Time) string {
|
||||
return time.Local().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
//将一个时间字符串转换成日期时间对象(yyyy-MM-dd HH:mm:ss)
|
||||
func ConvertTimeToTimeString(time time.Time) string {
|
||||
return time.Local().Format("15:04:05")
|
||||
}
|
||||
|
||||
//将一个时间字符串转换成日期对象(yyyy-MM-dd)
|
||||
func ConvertTimeToDateString(time time.Time) string {
|
||||
return time.Local().Format("2006-01-02")
|
||||
}
|
||||
|
||||
//一天中的最后一秒钟
|
||||
func LastSecondOfDay(day time.Time) time.Time {
|
||||
local, _ := time.LoadLocation("Local")
|
||||
return time.Date(day.Year(), day.Month(), day.Day(), 23, 59, 59, 0, local)
|
||||
}
|
||||
|
||||
//一天中的第一秒钟
|
||||
func FirstSecondOfDay(day time.Time) time.Time {
|
||||
local, _ := time.LoadLocation("Local")
|
||||
return time.Date(day.Year(), day.Month(), day.Day(), 0, 0, 0, 0, local)
|
||||
}
|
||||
|
||||
//一天中的第一分钟
|
||||
func FirstMinuteOfDay(day time.Time) time.Time {
|
||||
local, _ := time.LoadLocation("Local")
|
||||
return time.Date(day.Year(), day.Month(), day.Day(), 0, 1, 0, 0, local)
|
||||
}
|
||||
|
||||
//明天此刻的时间
|
||||
func Tomorrow() time.Time {
|
||||
tomorrow := time.Now()
|
||||
tomorrow = tomorrow.AddDate(0, 0, 1)
|
||||
return tomorrow
|
||||
}
|
||||
|
||||
//昨天此刻的时间
|
||||
func Yesterday() time.Time {
|
||||
tomorrow := time.Now()
|
||||
tomorrow = tomorrow.AddDate(0, 0, -1)
|
||||
return tomorrow
|
||||
}
|
10
code/tool/util_validation.go
Normal file
10
code/tool/util_validation.go
Normal file
@ -0,0 +1,10 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func ValidateEmail(email string) bool {
|
||||
emailRegexp := regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
|
||||
return emailRegexp.MatchString(email)
|
||||
}
|
50
code/upload_token_dao.go
Normal file
50
code/upload_token_dao.go
Normal file
@ -0,0 +1,50 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
|
||||
"github.com/nu7hatch/gouuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UploadTokenDao struct {
|
||||
BaseDao
|
||||
}
|
||||
|
||||
//按照Id查询
|
||||
func (this *UploadTokenDao) FindByUuid(uuid string) *UploadToken {
|
||||
|
||||
// Read
|
||||
var uploadToken = &UploadToken{}
|
||||
db := CONTEXT.DB.Where(&UploadToken{Base: Base{Uuid: uuid}}).First(uploadToken)
|
||||
if db.Error != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return uploadToken
|
||||
|
||||
}
|
||||
|
||||
//创建一个session并且持久化到数据库中。
|
||||
func (this *UploadTokenDao) Create(uploadToken *UploadToken) *UploadToken {
|
||||
|
||||
timeUUID, _ := uuid.NewV4()
|
||||
uploadToken.Uuid = string(timeUUID.String())
|
||||
|
||||
uploadToken.CreateTime = time.Now()
|
||||
uploadToken.UpdateTime = time.Now()
|
||||
uploadToken.Sort = time.Now().UnixNano() / 1e6
|
||||
db := CONTEXT.DB.Create(uploadToken)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return uploadToken
|
||||
}
|
||||
|
||||
//修改一个uploadToken
|
||||
func (this *UploadTokenDao) Save(uploadToken *UploadToken) *UploadToken {
|
||||
|
||||
uploadToken.UpdateTime = time.Now()
|
||||
db := CONTEXT.DB.Save(uploadToken)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return uploadToken
|
||||
}
|
22
code/upload_token_model.go
Normal file
22
code/upload_token_model.go
Normal file
@ -0,0 +1,22 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"tank/code/config"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UploadToken struct {
|
||||
Base
|
||||
UserUuid string `json:"userUuid"`
|
||||
FolderUuid string `json:"folderUuid"`
|
||||
MatterUuid string `json:"matterUuid"`
|
||||
ExpireTime time.Time `json:"expireTime"`
|
||||
Filename string `json:"filename"`
|
||||
Privacy bool `json:"privacy"`
|
||||
Size int64 `json:"size"`
|
||||
Ip string `json:"ip"`
|
||||
}
|
||||
|
||||
func (this *UploadToken) TableName() string {
|
||||
return config.TABLE_PREFIX + "upload_token"
|
||||
}
|
403
code/user_controller.go
Normal file
403
code/user_controller.go
Normal file
@ -0,0 +1,403 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"tank/code/config"
|
||||
"tank/code/result"
|
||||
"tank/code/tool"
|
||||
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserController struct {
|
||||
BaseController
|
||||
}
|
||||
|
||||
//初始化方法
|
||||
func (this *UserController) Init() {
|
||||
this.BaseController.Init()
|
||||
}
|
||||
|
||||
//注册自己的路由。
|
||||
func (this *UserController) RegisterRoutes() map[string]func(writer http.ResponseWriter, request *http.Request) {
|
||||
|
||||
routeMap := make(map[string]func(writer http.ResponseWriter, request *http.Request))
|
||||
|
||||
//每个Controller需要主动注册自己的路由。
|
||||
routeMap["/api/user/create"] = this.Wrap(this.Create, USER_ROLE_ADMINISTRATOR)
|
||||
routeMap["/api/user/edit"] = this.Wrap(this.Edit, USER_ROLE_USER)
|
||||
routeMap["/api/user/change/password"] = this.Wrap(this.ChangePassword, USER_ROLE_USER)
|
||||
routeMap["/api/user/reset/password"] = this.Wrap(this.ResetPassword, USER_ROLE_ADMINISTRATOR)
|
||||
routeMap["/api/user/login"] = this.Wrap(this.Login, USER_ROLE_GUEST)
|
||||
routeMap["/api/user/logout"] = this.Wrap(this.Logout, USER_ROLE_GUEST)
|
||||
routeMap["/api/user/detail"] = this.Wrap(this.Detail, USER_ROLE_USER)
|
||||
routeMap["/api/user/page"] = this.Wrap(this.Page, USER_ROLE_ADMINISTRATOR)
|
||||
routeMap["/api/user/disable"] = this.Wrap(this.Disable, USER_ROLE_ADMINISTRATOR)
|
||||
routeMap["/api/user/enable"] = this.Wrap(this.Enable, USER_ROLE_ADMINISTRATOR)
|
||||
|
||||
return routeMap
|
||||
}
|
||||
|
||||
//使用用户名和密码进行登录。
|
||||
//参数:
|
||||
// @email:邮箱
|
||||
// @password:密码
|
||||
func (this *UserController) Login(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
email := request.FormValue("email")
|
||||
password := request.FormValue("password")
|
||||
|
||||
if "" == email || "" == password {
|
||||
|
||||
panic(result.BadRequest("请输入邮箱和密码"))
|
||||
}
|
||||
|
||||
user := this.userDao.FindByEmail(email)
|
||||
if user == nil {
|
||||
|
||||
panic(result.BadRequest("邮箱或密码错误"))
|
||||
|
||||
} else {
|
||||
if !tool.MatchBcrypt(password, user.Password) {
|
||||
|
||||
panic(result.BadRequest("邮箱或密码错误"))
|
||||
}
|
||||
}
|
||||
|
||||
//登录成功,设置Cookie。有效期30天。
|
||||
expiration := time.Now()
|
||||
expiration = expiration.AddDate(0, 0, 30)
|
||||
|
||||
//持久化用户的session.
|
||||
session := &Session{
|
||||
UserUuid: user.Uuid,
|
||||
Ip: tool.GetIpAddress(request),
|
||||
ExpireTime: expiration,
|
||||
}
|
||||
session.UpdateTime = time.Now()
|
||||
session.CreateTime = time.Now()
|
||||
session = this.sessionDao.Create(session)
|
||||
|
||||
//设置用户的cookie.
|
||||
cookie := http.Cookie{
|
||||
Name: config.COOKIE_AUTH_KEY,
|
||||
Path: "/",
|
||||
Value: session.Uuid,
|
||||
Expires: expiration}
|
||||
http.SetCookie(writer, &cookie)
|
||||
|
||||
//更新用户上次登录时间和ip
|
||||
user.LastTime = time.Now()
|
||||
user.LastIp = tool.GetIpAddress(request)
|
||||
this.userDao.Save(user)
|
||||
|
||||
return this.Success(user)
|
||||
}
|
||||
|
||||
//创建一个用户
|
||||
func (this *UserController) Create(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
username := request.FormValue("username")
|
||||
if m, _ := regexp.MatchString(`^[0-9a-zA-Z_]+$`, username); !m {
|
||||
panic(`用户名必填,且只能包含字母,数字和'_''`)
|
||||
}
|
||||
password := request.FormValue("password")
|
||||
if len(password) < 6 {
|
||||
panic(`密码长度至少为6位`)
|
||||
}
|
||||
|
||||
email := request.FormValue("email")
|
||||
if email == "" {
|
||||
panic("邮箱必填!")
|
||||
}
|
||||
|
||||
avatarUrl := request.FormValue("avatarUrl")
|
||||
phone := request.FormValue("phone")
|
||||
gender := request.FormValue("gender")
|
||||
role := request.FormValue("role")
|
||||
city := request.FormValue("city")
|
||||
|
||||
//判断用户上传大小限制。
|
||||
sizeLimitStr := request.FormValue("sizeLimit")
|
||||
var sizeLimit int64 = 0
|
||||
if sizeLimitStr == "" {
|
||||
panic("用户上传限制必填!")
|
||||
} else {
|
||||
intsizeLimit, err := strconv.Atoi(sizeLimitStr)
|
||||
if err != nil {
|
||||
this.PanicError(err)
|
||||
}
|
||||
sizeLimit = int64(intsizeLimit)
|
||||
}
|
||||
|
||||
//判断重名。
|
||||
if this.userDao.CountByUsername(username) > 0 {
|
||||
panic(username + "已经被其他用户占用。")
|
||||
}
|
||||
//判断邮箱重名
|
||||
if this.userDao.CountByEmail(email) > 0 {
|
||||
panic(email + "已经被其他用户占用。")
|
||||
}
|
||||
|
||||
user := &User{
|
||||
Role: GetRole(role),
|
||||
Username: username,
|
||||
Password: tool.GetBcrypt(password),
|
||||
Email: email,
|
||||
Phone: phone,
|
||||
Gender: gender,
|
||||
City: city,
|
||||
AvatarUrl: avatarUrl,
|
||||
SizeLimit: sizeLimit,
|
||||
Status: USER_STATUS_OK,
|
||||
}
|
||||
|
||||
user = this.userDao.Create(user)
|
||||
|
||||
return this.Success(user)
|
||||
}
|
||||
|
||||
//编辑一个用户的资料。
|
||||
func (this *UserController) Edit(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
avatarUrl := request.FormValue("avatarUrl")
|
||||
uuid := request.FormValue("uuid")
|
||||
phone := request.FormValue("phone")
|
||||
gender := request.FormValue("gender")
|
||||
city := request.FormValue("city")
|
||||
|
||||
currentUser := this.checkUser(writer, request)
|
||||
user := this.userDao.CheckByUuid(uuid)
|
||||
|
||||
if currentUser.Role == USER_ROLE_ADMINISTRATOR {
|
||||
//只有管理员可以改变用户上传的大小
|
||||
//判断用户上传大小限制。
|
||||
sizeLimitStr := request.FormValue("sizeLimit")
|
||||
var sizeLimit int64 = 0
|
||||
if sizeLimitStr == "" {
|
||||
panic("用户上传限制必填!")
|
||||
} else {
|
||||
intsizeLimit, err := strconv.Atoi(sizeLimitStr)
|
||||
if err != nil {
|
||||
this.PanicError(err)
|
||||
}
|
||||
sizeLimit = int64(intsizeLimit)
|
||||
}
|
||||
user.SizeLimit = sizeLimit
|
||||
} else {
|
||||
if currentUser.Uuid != uuid {
|
||||
panic(result.Unauthorized("没有权限"))
|
||||
}
|
||||
}
|
||||
|
||||
user.AvatarUrl = avatarUrl
|
||||
user.Phone = phone
|
||||
user.Gender = GetGender(gender)
|
||||
user.City = city
|
||||
|
||||
user = this.userDao.Save(user)
|
||||
|
||||
return this.Success(user)
|
||||
}
|
||||
|
||||
//获取用户详情
|
||||
func (this *UserController) Detail(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
uuid := request.FormValue("uuid")
|
||||
|
||||
user := this.userDao.CheckByUuid(uuid)
|
||||
|
||||
return this.Success(user)
|
||||
|
||||
}
|
||||
|
||||
//退出登录
|
||||
func (this *UserController) Logout(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
//session置为过期
|
||||
sessionCookie, err := request.Cookie(config.COOKIE_AUTH_KEY)
|
||||
if err != nil {
|
||||
return this.Success("已经退出登录了!")
|
||||
}
|
||||
sessionId := sessionCookie.Value
|
||||
|
||||
user := this.findUser(writer, request)
|
||||
if user != nil {
|
||||
session := this.sessionDao.FindByUuid(sessionId)
|
||||
session.ExpireTime = time.Now()
|
||||
this.sessionDao.Save(session)
|
||||
}
|
||||
|
||||
//删掉session缓存
|
||||
_, err = CONTEXT.SessionCache.Delete(sessionId)
|
||||
if err != nil {
|
||||
this.logger.Error("删除用户session缓存时出错")
|
||||
}
|
||||
|
||||
//清空客户端的cookie.
|
||||
expiration := time.Now()
|
||||
expiration = expiration.AddDate(-1, 0, 0)
|
||||
cookie := http.Cookie{
|
||||
Name: config.COOKIE_AUTH_KEY,
|
||||
Path: "/",
|
||||
Value: sessionId,
|
||||
Expires: expiration}
|
||||
http.SetCookie(writer, &cookie)
|
||||
|
||||
return this.Success("退出成功!")
|
||||
}
|
||||
|
||||
//获取用户列表 管理员的权限。
|
||||
func (this *UserController) Page(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
pageStr := request.FormValue("page")
|
||||
pageSizeStr := request.FormValue("pageSize")
|
||||
orderCreateTime := request.FormValue("orderCreateTime")
|
||||
orderUpdateTime := request.FormValue("orderUpdateTime")
|
||||
orderSort := request.FormValue("orderSort")
|
||||
|
||||
username := request.FormValue("username")
|
||||
email := request.FormValue("email")
|
||||
phone := request.FormValue("phone")
|
||||
status := request.FormValue("status")
|
||||
orderLastTime := request.FormValue("orderLastTime")
|
||||
|
||||
var page int
|
||||
if pageStr != "" {
|
||||
page, _ = strconv.Atoi(pageStr)
|
||||
}
|
||||
|
||||
pageSize := 200
|
||||
if pageSizeStr != "" {
|
||||
tmp, err := strconv.Atoi(pageSizeStr)
|
||||
if err == nil {
|
||||
pageSize = tmp
|
||||
}
|
||||
}
|
||||
|
||||
sortArray := []OrderPair{
|
||||
{
|
||||
key: "create_time",
|
||||
value: orderCreateTime,
|
||||
},
|
||||
{
|
||||
key: "update_time",
|
||||
value: orderUpdateTime,
|
||||
},
|
||||
{
|
||||
key: "sort",
|
||||
value: orderSort,
|
||||
},
|
||||
{
|
||||
key: "last_time",
|
||||
value: orderLastTime,
|
||||
},
|
||||
}
|
||||
|
||||
pager := this.userDao.Page(page, pageSize, username, email, phone, status, sortArray)
|
||||
|
||||
return this.Success(pager)
|
||||
}
|
||||
|
||||
//禁用用户
|
||||
func (this *UserController) Disable(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
uuid := request.FormValue("uuid")
|
||||
|
||||
user := this.userDao.CheckByUuid(uuid)
|
||||
|
||||
loginUser := this.checkUser(writer, request)
|
||||
if uuid == loginUser.Uuid {
|
||||
panic(result.BadRequest("你不能操作自己的状态。"))
|
||||
}
|
||||
|
||||
if user.Status == USER_STATUS_DISABLED {
|
||||
panic(result.BadRequest("用户已经被禁用,操作无效。"))
|
||||
}
|
||||
|
||||
user.Status = USER_STATUS_DISABLED
|
||||
|
||||
user = this.userDao.Save(user)
|
||||
|
||||
return this.Success(user)
|
||||
|
||||
}
|
||||
|
||||
//启用用户
|
||||
func (this *UserController) Enable(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
uuid := request.FormValue("uuid")
|
||||
|
||||
user := this.userDao.CheckByUuid(uuid)
|
||||
loginUser := this.checkUser(writer, request)
|
||||
if uuid == loginUser.Uuid {
|
||||
panic(result.BadRequest("你不能操作自己的状态。"))
|
||||
}
|
||||
|
||||
if user.Status == USER_STATUS_OK {
|
||||
panic(result.BadRequest("用户已经是正常状态,操作无效。"))
|
||||
}
|
||||
|
||||
user.Status = USER_STATUS_OK
|
||||
|
||||
user = this.userDao.Save(user)
|
||||
|
||||
return this.Success(user)
|
||||
|
||||
}
|
||||
|
||||
//用户修改密码
|
||||
func (this *UserController) ChangePassword(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
oldPassword := request.FormValue("oldPassword")
|
||||
newPassword := request.FormValue("newPassword")
|
||||
if oldPassword == "" || newPassword == "" {
|
||||
panic(result.BadRequest("旧密码和新密码都不能为空"))
|
||||
}
|
||||
|
||||
user := this.checkUser(writer, request)
|
||||
|
||||
//如果是demo账号,不提供修改密码的功能。
|
||||
if user.Username == "demo" {
|
||||
return this.Success(user)
|
||||
}
|
||||
|
||||
if !tool.MatchBcrypt(oldPassword, user.Password) {
|
||||
panic(result.BadRequest("旧密码不正确!"))
|
||||
}
|
||||
|
||||
user.Password = tool.GetBcrypt(newPassword)
|
||||
|
||||
user = this.userDao.Save(user)
|
||||
|
||||
return this.Success(user)
|
||||
}
|
||||
|
||||
//管理员重置用户密码
|
||||
func (this *UserController) ResetPassword(writer http.ResponseWriter, request *http.Request) *result.WebResult {
|
||||
|
||||
userUuid := request.FormValue("userUuid")
|
||||
password := request.FormValue("password")
|
||||
if userUuid == "" {
|
||||
panic(result.BadRequest("用户不能为空"))
|
||||
}
|
||||
if password == "" {
|
||||
panic(result.BadRequest("密码不能为空"))
|
||||
}
|
||||
|
||||
currentUser := this.checkUser(writer, request)
|
||||
|
||||
if currentUser.Role != USER_ROLE_ADMINISTRATOR {
|
||||
panic(result.Unauthorized("没有权限"))
|
||||
}
|
||||
|
||||
user := this.userDao.CheckByUuid(userUuid)
|
||||
|
||||
user.Password = tool.GetBcrypt(password)
|
||||
|
||||
user = this.userDao.Save(user)
|
||||
|
||||
return this.Success(currentUser)
|
||||
}
|
157
code/user_dao.go
Normal file
157
code/user_dao.go
Normal file
@ -0,0 +1,157 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"github.com/nu7hatch/gouuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserDao struct {
|
||||
BaseDao
|
||||
}
|
||||
|
||||
//创建用户
|
||||
func (this *UserDao) Create(user *User) *User {
|
||||
|
||||
if user == nil {
|
||||
panic("参数不能为nil")
|
||||
}
|
||||
|
||||
timeUUID, _ := uuid.NewV4()
|
||||
user.Uuid = string(timeUUID.String())
|
||||
user.CreateTime = time.Now()
|
||||
user.UpdateTime = time.Now()
|
||||
user.LastTime = time.Now()
|
||||
user.Sort = time.Now().UnixNano() / 1e6
|
||||
|
||||
db := CONTEXT.DB.Create(user)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
//按照Id查询用户,找不到返回nil
|
||||
func (this *UserDao) FindByUuid(uuid string) *User {
|
||||
|
||||
// Read
|
||||
var user *User = &User{}
|
||||
db := CONTEXT.DB.Where(&User{Base: Base{Uuid: uuid}}).First(user)
|
||||
if db.Error != nil {
|
||||
return nil
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
//按照Id查询用户,找不到抛panic
|
||||
func (this *UserDao) CheckByUuid(uuid string) *User {
|
||||
|
||||
if uuid == "" {
|
||||
panic("uuid必须指定")
|
||||
}
|
||||
|
||||
// Read
|
||||
var user = &User{}
|
||||
db := CONTEXT.DB.Where(&User{Base: Base{Uuid: uuid}}).First(user)
|
||||
this.PanicError(db.Error)
|
||||
return user
|
||||
}
|
||||
|
||||
//按照邮箱查询用户。
|
||||
func (this *UserDao) FindByUsername(username string) *User {
|
||||
|
||||
var user = &User{}
|
||||
db := CONTEXT.DB.Where(&User{Username: username}).First(user)
|
||||
if db.Error != nil {
|
||||
return nil
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
//按照邮箱查询用户。
|
||||
func (this *UserDao) FindByEmail(email string) *User {
|
||||
|
||||
var user *User = &User{}
|
||||
db := CONTEXT.DB.Where(&User{Email: email}).First(user)
|
||||
if db.Error != nil {
|
||||
return nil
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
//显示用户列表。
|
||||
func (this *UserDao) Page(page int, pageSize int, username string, email string, phone string, status string, sortArray []OrderPair) *Pager {
|
||||
|
||||
var wp = &WherePair{}
|
||||
|
||||
if username != "" {
|
||||
wp = wp.And(&WherePair{Query: "username LIKE ?", Args: []interface{}{"%" + username + "%"}})
|
||||
}
|
||||
|
||||
if email != "" {
|
||||
wp = wp.And(&WherePair{Query: "email LIKE ?", Args: []interface{}{"%" + email + "%"}})
|
||||
}
|
||||
|
||||
if phone != "" {
|
||||
wp = wp.And(&WherePair{Query: "phone = ?", Args: []interface{}{phone}})
|
||||
}
|
||||
|
||||
if status != "" {
|
||||
wp = wp.And(&WherePair{Query: "status = ?", Args: []interface{}{status}})
|
||||
}
|
||||
|
||||
count := 0
|
||||
db := CONTEXT.DB.Model(&User{}).Where(wp.Query, wp.Args...).Count(&count)
|
||||
this.PanicError(db.Error)
|
||||
|
||||
var users []*User
|
||||
orderStr := this.GetSortString(sortArray)
|
||||
if orderStr == "" {
|
||||
db = CONTEXT.DB.Where(wp.Query, wp.Args...).Offset(page * pageSize).Limit(pageSize).Find(&users)
|
||||
} else {
|
||||
db = CONTEXT.DB.Where(wp.Query, wp.Args...).Order(orderStr).Offset(page * pageSize).Limit(pageSize).Find(&users)
|
||||
}
|
||||
|
||||
this.PanicError(db.Error)
|
||||
|
||||
pager := NewPager(page, pageSize, count, users)
|
||||
|
||||
return pager
|
||||
}
|
||||
|
||||
//查询某个用户名是否已经有用户了
|
||||
func (this *UserDao) CountByUsername(username string) int {
|
||||
var count int
|
||||
db := CONTEXT.DB.
|
||||
Model(&User{}).
|
||||
Where("username = ?", username).
|
||||
Count(&count)
|
||||
this.PanicError(db.Error)
|
||||
return count
|
||||
}
|
||||
|
||||
//查询某个邮箱是否已经有用户了
|
||||
func (this *UserDao) CountByEmail(email string) int {
|
||||
var count int
|
||||
db := CONTEXT.DB.
|
||||
Model(&User{}).
|
||||
Where("email = ?", email).
|
||||
Count(&count)
|
||||
this.PanicError(db.Error)
|
||||
return count
|
||||
}
|
||||
|
||||
//保存用户
|
||||
func (this *UserDao) Save(user *User) *User {
|
||||
|
||||
user.UpdateTime = time.Now()
|
||||
db := CONTEXT.DB.
|
||||
Save(user)
|
||||
this.PanicError(db.Error)
|
||||
return user
|
||||
}
|
||||
|
||||
//执行清理操作
|
||||
func (this *UserDao) Cleanup() {
|
||||
this.logger.Info("[UserDao]执行清理:清除数据库中所有User记录。")
|
||||
db := CONTEXT.DB.Where("uuid is not null and role != ?", USER_ROLE_ADMINISTRATOR).Delete(User{})
|
||||
this.PanicError(db.Error)
|
||||
}
|
67
code/user_model.go
Normal file
67
code/user_model.go
Normal file
@ -0,0 +1,67 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"tank/code/config"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
//游客身份
|
||||
USER_ROLE_GUEST = "GUEST"
|
||||
//普通注册用户
|
||||
USER_ROLE_USER = "USER"
|
||||
//管理员
|
||||
USER_ROLE_ADMINISTRATOR = "ADMINISTRATOR"
|
||||
)
|
||||
|
||||
const (
|
||||
USER_GENDER_MALE = "MALE"
|
||||
USER_GENDER_FEMALE = "FEMALE"
|
||||
USER_GENDER_UNKNOWN = "UNKNOWN"
|
||||
)
|
||||
|
||||
const (
|
||||
//正常状态
|
||||
USER_STATUS_OK = "OK"
|
||||
//被禁用
|
||||
USER_STATUS_DISABLED = "DISABLED"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Base
|
||||
Role string `json:"role" gorm:"type:varchar(45)"`
|
||||
Username string `json:"username" gorm:"type:varchar(45) not null;unique"`
|
||||
Password string `json:"-" gorm:"type:varchar(255)"`
|
||||
Email string `json:"email" gorm:"type:varchar(45) not null;unique"`
|
||||
Phone string `json:"phone" gorm:"type:varchar(45)"`
|
||||
Gender string `json:"gender" gorm:"type:varchar(45)"`
|
||||
City string `json:"city" gorm:"type:varchar(45)"`
|
||||
AvatarUrl string `json:"avatarUrl" gorm:"type:varchar(255)"`
|
||||
LastIp string `json:"lastIp" gorm:"type:varchar(128)"`
|
||||
LastTime time.Time `json:"lastTime" gorm:"type:timestamp not null;default:'2018-01-01 00:00:00'"`
|
||||
SizeLimit int64 `json:"sizeLimit" gorm:"type:bigint(20) not null;default:-1"`
|
||||
Status string `json:"status" gorm:"type:varchar(45)"`
|
||||
}
|
||||
|
||||
// set User's table name to be `profiles`
|
||||
func (this *User) TableName() string {
|
||||
return config.TABLE_PREFIX + "user"
|
||||
}
|
||||
|
||||
//通过一个字符串获取性别
|
||||
func GetGender(genderString string) string {
|
||||
if genderString == USER_GENDER_MALE || genderString == USER_GENDER_FEMALE || genderString == USER_GENDER_UNKNOWN {
|
||||
return genderString
|
||||
} else {
|
||||
return USER_GENDER_UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
//通过一个字符串获取角色
|
||||
func GetRole(roleString string) string {
|
||||
if roleString == USER_ROLE_USER || roleString == USER_ROLE_ADMINISTRATOR {
|
||||
return roleString
|
||||
} else {
|
||||
return USER_ROLE_USER
|
||||
}
|
||||
}
|
114
code/user_service.go
Normal file
114
code/user_service.go
Normal file
@ -0,0 +1,114 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"tank/code/cache"
|
||||
"tank/code/config"
|
||||
"tank/code/result"
|
||||
"time"
|
||||
)
|
||||
|
||||
//@Service
|
||||
type UserService struct {
|
||||
Bean
|
||||
userDao *UserDao
|
||||
sessionDao *SessionDao
|
||||
|
||||
//操作文件的锁。
|
||||
locker *cache.CacheTable
|
||||
}
|
||||
|
||||
//初始化方法
|
||||
func (this *UserService) Init() {
|
||||
this.Bean.Init()
|
||||
|
||||
//手动装填本实例的Bean. 这里必须要用中间变量方可。
|
||||
b := CONTEXT.GetBean(this.userDao)
|
||||
if b, ok := b.(*UserDao); ok {
|
||||
this.userDao = b
|
||||
}
|
||||
|
||||
b = CONTEXT.GetBean(this.sessionDao)
|
||||
if b, ok := b.(*SessionDao); ok {
|
||||
this.sessionDao = b
|
||||
}
|
||||
|
||||
//创建一个用于存储用户文件锁的缓存。
|
||||
this.locker = cache.NewCacheTable()
|
||||
}
|
||||
|
||||
|
||||
//对某个用户进行加锁。加锁阶段用户是不允许操作文件的。
|
||||
func (this *UserService) MatterLock(userUuid string) {
|
||||
//如果已经是锁住的状态,直接报错
|
||||
|
||||
//去缓存中捞取
|
||||
cacheItem, err := this.locker.Value(userUuid)
|
||||
if err != nil {
|
||||
this.logger.Error("获取缓存时出错了" + err.Error())
|
||||
}
|
||||
|
||||
//当前被锁住了。
|
||||
if cacheItem != nil && cacheItem.Data() != nil {
|
||||
panic(result.BadRequest("当前正在进行文件操作,请稍后再试!"))
|
||||
}
|
||||
|
||||
//添加一把新锁,有效期为12小时
|
||||
duration := 12 * time.Hour
|
||||
this.locker.Add(userUuid, duration, true)
|
||||
}
|
||||
|
||||
|
||||
//对某个用户解锁,解锁后用户可以操作文件。
|
||||
func (this *UserService) MatterUnlock(userUuid string) {
|
||||
|
||||
exist := this.locker.Exists(userUuid)
|
||||
if exist {
|
||||
_, err := this.locker.Delete(userUuid)
|
||||
this.PanicError(err)
|
||||
} else {
|
||||
this.logger.Error("%s已经不存在matter锁了,解锁错误。", userUuid)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//装载session信息,如果session没有了根据cookie去装填用户信息。
|
||||
//在所有的路由最初会调用这个方法
|
||||
func (this *UserService) bootstrap(writer http.ResponseWriter, request *http.Request) {
|
||||
|
||||
//登录身份有效期以数据库中记录的为准
|
||||
|
||||
//验证用户是否已经登录。
|
||||
sessionCookie, err := request.Cookie(config.COOKIE_AUTH_KEY)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sessionId := sessionCookie.Value
|
||||
|
||||
//去缓存中捞取
|
||||
cacheItem, err := CONTEXT.SessionCache.Value(sessionId)
|
||||
if err != nil {
|
||||
this.logger.Error("获取缓存时出错了" + err.Error())
|
||||
}
|
||||
|
||||
//缓存中没有,尝试去数据库捞取
|
||||
if cacheItem == nil || cacheItem.Data() == nil {
|
||||
session := this.sessionDao.FindByUuid(sessionCookie.Value)
|
||||
if session != nil {
|
||||
duration := session.ExpireTime.Sub(time.Now())
|
||||
if duration <= 0 {
|
||||
this.logger.Error("登录信息已过期")
|
||||
} else {
|
||||
user := this.userDao.FindByUuid(session.UserUuid)
|
||||
if user != nil {
|
||||
//将用户装填进缓存中
|
||||
CONTEXT.SessionCache.Add(sessionCookie.Value, duration, user)
|
||||
} else {
|
||||
this.logger.Error("没有找到对应的user " + session.UserUuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user