Rename rest to code.

This commit is contained in:
zicla
2019-04-26 02:59:35 +08:00
parent 46256439da
commit ce84a9c9f0
67 changed files with 138 additions and 124 deletions

425
code/alien_controller.go Normal file
View 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
View 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
View 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 {
//返回的内容申明是jsonutf-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
View File

@ -0,0 +1,8 @@
package code
type BaseDao struct {
Bean
}

64
code/base_model.go Normal file
View 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
View 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
View 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
View 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
View 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()
}

View 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
View 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, &times)
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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

692
code/dav/xml/read.go Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

174
code/dav_controller.go Normal file
View 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
View 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
View 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
View 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)
}
}

View 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)
}

View 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"
}

View 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
View 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
View 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
View 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))
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}
}

View 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
View 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
View 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"
}

View 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
View 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
View 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格式 返回的内容申明是jsonutf-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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
View 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
}

View 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
View 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
View 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
View 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
View 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)
}
}
}
}
}