diff --git a/app/controllers/AttachController.go b/app/controllers/AttachController.go index d7f2134..97d18f9 100644 --- a/app/controllers/AttachController.go +++ b/app/controllers/AttachController.go @@ -100,7 +100,7 @@ func (c Attach) uploadAttach(noteId string) (re info.Re) { id := bson.NewObjectId(); fileInfo.AttachId = id fileId = id.Hex() - Ok, resultMsg = attachService.AddAttach(fileInfo) + Ok, resultMsg = attachService.AddAttach(fileInfo, false) if resultMsg != "" { resultMsg = c.Message(resultMsg) } diff --git a/app/controllers/BlogController.go b/app/controllers/BlogController.go index 98c6c36..b5bf765 100644 --- a/app/controllers/BlogController.go +++ b/app/controllers/BlogController.go @@ -200,29 +200,77 @@ func (c Blog) getCates(userBlog info.UserBlog) { } var i = 0 - cates := make([]map[string]string, len(notebooks)) + cates := make([]*info.Cate, len(notebooks)) // 先要保证已有的是正确的排序 cateIds := userBlog.CateIds has := map[string]bool{} // cateIds中有的 + cateMap := map[string]*info.Cate{} if cateIds != nil && len(cateIds) > 0 { for _, cateId := range cateIds { if n, ok := notebooksMap[cateId]; ok { - cates[i] = map[string]string{"Title": n.Title, "UrlTitle": c.getCateUrlTitle(&n), "CateId": n.NotebookId.Hex()} + parentNotebookId := "" + if n.ParentNotebookId != "" { + parentNotebookId = n.ParentNotebookId.Hex() + } + cates[i] = &info.Cate{Title: n.Title, UrlTitle: c.getCateUrlTitle(&n), CateId: n.NotebookId.Hex(), ParentCateId: parentNotebookId} + cateMap[cates[i].CateId] = cates[i] i++ has[cateId] = true } } } - // 之后 + + // 之后添加没有排序的 for _, n := range notebooks { id := n.NotebookId.Hex() if !has[id] { - cates[i] = map[string]string{"Title": n.Title, "UrlTitle": c.getCateUrlTitle(&n), "CateId": id} + parentNotebookId := "" + if n.ParentNotebookId != "" { + parentNotebookId = n.ParentNotebookId.Hex() + } + cates[i] = &info.Cate{Title: n.Title, UrlTitle: c.getCateUrlTitle(&n), CateId: id, ParentCateId: parentNotebookId} + cateMap[cates[i].CateId] = cates[i] i++ } } + +// LogJ(">>") +// LogJ(cates) + + // 建立层级 + hasParent := map[string]bool{} // 有父的cate + for _, cate := range cates { + parentCateId := cate.ParentCateId + if parentCateId != "" { + if parentCate, ok := cateMap[parentCateId]; ok { +// Log("________") +// LogJ(parentCate) +// LogJ(cate) + if parentCate.Children == nil { + parentCate.Children = []*info.Cate{cate} + } else { + parentCate.Children = append(parentCate.Children, cate) + } + hasParent[cate.CateId] = true + } + } + } + + // 得到没有父的cate, 作为第一级cate + catesTree := []*info.Cate{} + for _, cate := range cates { + if !hasParent[cate.CateId] { + catesTree = append(catesTree, cate) + } + } + + Log("cates") + LogJ(cates) + LogJ(catesTree); + c.RenderArgs["cates"] = cates + c.RenderArgs["catesTree"] = catesTree } // 单页 diff --git a/app/controllers/NoteController.go b/app/controllers/NoteController.go index 59c43bb..94d1c09 100644 --- a/app/controllers/NoteController.go +++ b/app/controllers/NoteController.go @@ -7,6 +7,7 @@ import ( . "github.com/leanote/leanote/app/lea" "github.com/leanote/leanote/app/info" "os/exec" + "strings" // "time" // "github.com/leanote/leanote/app/types" // "io/ioutil" @@ -124,9 +125,7 @@ func (c Note) Index(noteId string) revel.Result { c.RenderArgs["globalConfigs"] = configService.GetGlobalConfigForUser() - -// return c.RenderTemplate("note/note.html") - +// return c.RenderTemplate("note/note.html") if isDev, _ := revel.Config.Bool("mode.dev"); isDev { return c.RenderTemplate("note/note-dev.html") } else { @@ -169,7 +168,7 @@ type NoteOrContent struct { Title string Desc string ImgSrc string - Tags []string + Tags string Content string Abstract string IsNew bool @@ -192,7 +191,7 @@ func (c Note) UpdateNoteOrContent(noteOrContent NoteOrContent) revel.Result { NoteId: bson.ObjectIdHex(noteOrContent.NoteId), NotebookId: bson.ObjectIdHex(noteOrContent.NotebookId), Title: noteOrContent.Title, - Tags: noteOrContent.Tags, + Tags: strings.Split(noteOrContent.Tags, ","), Desc: noteOrContent.Desc, ImgSrc: noteOrContent.ImgSrc, IsBlog: noteOrContent.IsBlog, @@ -225,23 +224,32 @@ func (c Note) UpdateNoteOrContent(noteOrContent NoteOrContent) revel.Result { noteUpdate["Title"] = noteOrContent.Title; } - if c.Has("Tags[]") { + if c.Has("Tags") { needUpdateNote = true - noteUpdate["Tags"] = noteOrContent.Tags; + noteUpdate["Tags"] = strings.Split(noteOrContent.Tags, ","); } + // web端不控制 if needUpdateNote { - noteService.UpdateNote(noteOrContent.UserId, c.GetUserId(), - noteOrContent.NoteId, noteUpdate) + noteService.UpdateNote(c.GetUserId(), + noteOrContent.NoteId, noteUpdate, -1) } //------------- - + afterContentUsn := 0 + contentOk := false + contentMsg := "" if c.Has("Content") { - noteService.UpdateNoteContent(noteOrContent.UserId, c.GetUserId(), - noteOrContent.NoteId, noteOrContent.Content, noteOrContent.Abstract) +// noteService.UpdateNoteContent(noteOrContent.UserId, c.GetUserId(), +// noteOrContent.NoteId, noteOrContent.Content, noteOrContent.Abstract) + contentOk, contentMsg, afterContentUsn = noteService.UpdateNoteContent(c.GetUserId(), + noteOrContent.NoteId, noteOrContent.Content, noteOrContent.Abstract, needUpdateNote, -1) } + Log(afterContentUsn) + Log(contentOk) + Log(contentMsg) + return c.RenderJson(true) } @@ -291,10 +299,12 @@ func (c Note) SearchNoteByTags(tags []string) revel.Result { // 这是脚本调用, 没有cookie, 不执行权限控制, 通过传来的appKey判断 func (c Note) ToImage(noteId, appKey string) revel.Result { // 虽然传了cookie但是这里还是不能得到userId, 所以还是通过appKey来验证之 + /* appKeyTrue, _ := revel.Config.String("app.secret") if appKeyTrue != appKey { return c.RenderText("") } + */ note := noteService.GetNoteById(noteId) if note.NoteId == "" { return c.RenderText("") @@ -413,6 +423,75 @@ func (c Note) Html2Image(noteId string) revel.Result { } +// 导出成PDF +func (c Note) ExportPdf(noteId string) revel.Result { + re := info.NewRe() + userId := c.GetUserId() + note := noteService.GetNoteById(noteId) + if note.NoteId == "" { + re.Msg = "No Note" + return c.RenderJson(re) + } + + noteUserId := note.UserId.Hex() + // 是否有权限 + if noteUserId != userId { + // 是否是有权限协作的 + if !note.IsBlog && !shareService.HasReadPerm(noteUserId, userId, noteId) { + re.Msg = "No Perm" + return c.RenderJson(re) + } + } + + // path 判断是否需要重新生成之 + fileUrlPath := "upload/" + noteUserId + "/images/weibo" + dir := revel.BasePath + "/public/" + fileUrlPath + if !ClearDir(dir) { + re.Msg = "No Dir" + return c.RenderJson(re) + } + filename := note.Title + ".pdf"; + if note.Title == "" { + filename = "Untitled.pdf"; + } + path := dir + "/" + filename + + // cookie + cookieName := revel.CookiePrefix + "_SESSION" + cookie, err := c.Request.Cookie(cookieName) + cookieStr := cookie.String() + cookieValue := "" + if err == nil && len(cookieStr) > len(cookieName) { + cookieValue = cookieStr[len(cookieName)+1:] + } + Log(cookieValue) + + appKey, _ := revel.Config.String("app.secret") +// cookieDomain, _ := revel.Config.String("cookie.domain") + // 生成之 + url := configService.GetSiteUrl() + "/note/toImage?noteId=" + noteId + "&appKey=" + appKey; + // /Users/life/Documents/bin/phantomjs/bin/phantomjs /Users/life/Desktop/test/b.js + binPath := "/usr/local/bin/wkhtmltopdf" // configService.GetGlobalStringConfig("toImageBinPath") + if binPath == "" { + return c.RenderJson(re); + } +// cc := binPath + " \"" + url + "\" \"" + path + "\" \"" + cookieDomain + "\" \"" + cookieName + "\" \"" + cookieValue + "\"" + cc := binPath + " \"" + url + "\" \"" + path + "\"" // \"" + cookieDomain + "\" \"" + cookieName + "\" \"" + cookieValue + "\"" + cmd := exec.Command("/bin/sh", "-c", cc) + Log(cc); + b, err := cmd.Output() + if err == nil { + re.Ok = true + re.Id = fileUrlPath + "/" + filename + } else { + re.Msg = string(b) + Log("error:......") + Log(string(b)) + } + + return c.RenderJson(re) +} + // 设置/取消Blog; 置顶 func (c Note) SetNote2Blog(noteId string, isBlog, isTop bool) revel.Result { re := noteService.ToBlog(c.GetUserId(), noteId, isBlog, isTop) diff --git a/app/controllers/NotebookController.go b/app/controllers/NotebookController.go index db1d7d9..5d2ab4c 100644 --- a/app/controllers/NotebookController.go +++ b/app/controllers/NotebookController.go @@ -37,7 +37,8 @@ func (c Notebook) AddNotebook(notebookId, title, parentNotebookId string) revel. if(parentNotebookId != "") { notebook.ParentNotebookId = bson.ObjectIdHex(parentNotebookId) } - re := notebookService.AddNotebook(notebook) + + re, notebook := notebookService.AddNotebook(notebook) if(re) { return c.RenderJson(notebook) @@ -74,4 +75,4 @@ func (c Notebook) DragNotebooks(data string) revel.Result { func (c Notebook) SetNotebook2Blog(notebookId string, isBlog bool) revel.Result { re := notebookService.ToBlog(c.GetUserId(), notebookId, isBlog) return c.RenderJson(re) -} \ No newline at end of file +} diff --git a/app/controllers/TagController.go b/app/controllers/TagController.go new file mode 100644 index 0000000..96778dc --- /dev/null +++ b/app/controllers/TagController.go @@ -0,0 +1,30 @@ +package controllers + +import ( + "github.com/revel/revel" +// "encoding/json" +// "gopkg.in/mgo.v2/bson" +// . "github.com/leanote/leanote/app/lea" + "github.com/leanote/leanote/app/info" +// "os/exec" +) + +type Tag struct { + BaseController +} + +// 更新Tag +func (c Tag) UpdateTag(tag string) revel.Result { + ret := info.NewRe() + ret.Ok = true + ret.Item = tagService.AddOrUpdateTag(c.GetUserId(), tag) + return c.RenderJson(ret) +} + +// 删除标签 +func (c Tag) DeleteTag(tag string) revel.Result { + ret := info.Re{} + ret.Ok = true + ret.Item = tagService.DeleteTag(c.GetUserId(), tag) + return c.RenderJson(ret) +} \ No newline at end of file diff --git a/app/controllers/admin/AdminUpgradeController.go b/app/controllers/admin/AdminUpgradeController.go index 3835d68..8f8f2f3 100644 --- a/app/controllers/admin/AdminUpgradeController.go +++ b/app/controllers/admin/AdminUpgradeController.go @@ -21,4 +21,10 @@ func (c AdminUpgrade) UpgradeBetaToBeta2() revel.Result { re := info.NewRe() re.Ok, re.Msg = upgradeService.UpgradeBetaToBeta2(c.GetUserId()) return c.RenderJson(re) +} + +func (c AdminUpgrade) UpgradeBeta3ToBeta4() revel.Result { + re := info.NewRe() + re.Ok, re.Msg = upgradeService.Api(c.GetUserId()) + return c.RenderJson(re) } \ No newline at end of file diff --git a/app/controllers/api/API列表-v0.1.md b/app/controllers/api/API列表-v0.1.md new file mode 100644 index 0000000..d4957d5 --- /dev/null +++ b/app/controllers/api/API列表-v0.1.md @@ -0,0 +1,463 @@ +# API 列表 + +## 前言 + +### api url + +所有api的url前面带/api/, 如: + +`/api/user/info?userId=xxxx&token=xxxx` + +除了/auth/login, /auth/register外其它的都需要另外带参数token=xxxx + +### 文件目录结构 +* 所有API的Controller都在app/api文件夹下 +* 文件命名: Api功能Controller.go, 如ApiUserController.go +* 结构体命名为 Api功能, 如ApiUser +* API公用Controller: ApiBaseController +* init.go 注入service和定义拦截器 + +### 流程 +用户登录后返回一个token, 以后所有的请求都携带该token. +在init.go中的拦截器会得到token并调用sessionService判断是否登录了 + +## 返回值结构 +* 全部返回JSON, JSON, 除二进制文件(图片, 附件外), 如果返回其它非JSON格式的值, 肯定是出错了 +* 错误信息全部返回 {Ok: false, Msg: "相应的错误信息"} +* 正确信息返回分两种: +1. 一些操作型的api, 比如updateUsername, updateLogo之类的, 成功后返回 {Ok: true, Msg:""} +2. 一些获取型api, 如getNote, 全部返回真实的返回数据, 如返回笔记: +``` +{ + "NoteId": "54bdc7e305fcd13ea3000000", + "NotebookId": "54bdc65599c37b0da9000003", + "UserId": "54bdc65599c37b0da9000002", + "Title": "笔记标题", + "Desc": "", + "Tags": null, + "Abstract": "", + "Content": "", + "IsMarkdown": false, + "IsBlog": false, + "IsTrash": true, + "IsDeleted": false, + "Usn": 15, + "Files": [], + "CreatedTime": "2015-01-20T11:13:41.34+08:00", + "UpdatedTime": "2015-01-20T11:13:41.34+08:00", + "PublicTime": "0001-01-01T00:00:00Z" +} +``` +* 时间类型全是返回如 "2015-01-20T11:13:41.34+08:00" 的格式, (因为golang的time转成Json就是这样, 历史原因) + +----------------- + +## Auth 登录与注册 + +### /auth/login 登录 + +``` +参数: email, pwd +Method: GET +返回: +错误: {"Ok":false, "Msg":"用户名或密码有误"} +正确: 比如: +{ + "Ok":true, + "Token":"5500830738f41138e90003232", + "UserId":"52d26b4e99c37b609a000001", + "Email":"leanote@leanote.com", + "Username":"leanote" +} +``` + +登录成功后将使用token作为之后的请求 + +### /auth/logout 注销 +``` +参数: token +Method: GET +返回: +错误: {Ok: false, Msg: ""} +成功: {Ok: true, Msg: ""} +``` + +### /auth/register 注册 +``` +参数: email, pwd +Method: POST +返回: +错误: {Ok: false, Msg: ""} +成功: {Ok: true, Msg: ""} +``` + +## User 用户 +### /user/info 获取用户信息 +``` +参数: userId +Method: GET +返回: +错误: {Ok: false, Msg: ""} +成功: type.User +``` + +### /user/updateUsername 修改用户名 +``` +参数: username(新用户名) +Method: POST +返回: +错误: {Ok: false, Msg: ""} +成功: {Ok: true, Msg: ""} +``` + +### /user/updatePwd 修改密码 +``` +参数: oldPwd(旧密码), pwd(新密码) +Method: POST +返回: +错误: {Ok: false, Msg: ""} +成功: {Ok: true, Msg: ""} +``` + +### /user/updateLogo 修改头像 +``` +参数: file(文件) +Method: POST +返回: +错误: {Ok: false, Msg: ""} +成功: {Ok: true, Msg: ""} +``` + +### /user/getSyncState 获取最新同步状态 +``` +参数: 无 +Method: POST +返回: +错误: {Ok: false, Msg: ""} +成功: {LastSyncUsn: 3232, LastSyncTime: "上次同步时间"(暂时无用)} +``` + +----- + +## Notebook 笔记本 + +### /notebook/getSyncNotebooks 得到需要同步的笔记本 +``` +参数: afterUsn(int, 在此usn后的笔记本是需要同步的), maxEntry(int, 最大要同步的量) +Method: GET +返回: +错误: {Ok: false, Msg: ""} +成功: [type.Notebook] 数组 +``` + +### /notebook/getNotebooks 得到所有笔记本 +``` +无参数 +Method: GET +返回: +错误: {Ok: false, Msg: ""} +成功: [type.Notebook] 数组 +``` + +### /notebook/addNotebook 添加笔记本 +``` +参数: title(string), parentNotebookId(string, 父notebookId, 可空), seq(int) 排列 +Method: POST +返回: +错误: {Ok: false, Msg:""} +成功: type.Notebook +``` + +### /notebook/updateNotebook 修改笔记本 +``` +参数: notebookId, title, parentNotebookId, seq(int), usn(int) +Method: POST +返回: +错误: {Ok: false, msg: ""} msg == "conflict" 表示冲突 +成功: type.Notebook +``` + +### /notebook/deleteNotebook 删除笔记本 +``` +参数: notebookId, usn(int) +Method: GET +返回: +错误: {Ok: false, msg: ""} msg == "conflict" 表示冲突 +成功: {Ok: true} +``` + +---- + +## Note 笔记 + + +### /note/getSyncNotes 获取需要同步的笔记 +``` +参数: afterUsn(int, 在此usn后的笔记是需要同步的), maxEntry(int, 最大要同步的量) +Method: GET +返回: +错误: {Ok: false, Msg: ""} +成功: [type.Note] 数组, 笔记不包含Abstract和Content +``` + + +### /note/getNotes 获得某笔记本下的笔记(无内容) +``` +参数: notebookId +Method: GET +返回: +错误: {Ok: false, Msg: ""} +成功: [type.Note] 数组, 笔记不包含Abstract和Content +``` + +### /note/getNoteAndContent 获得笔记与内容 +``` +参数: noteId +Method: GET +返回: +错误: {Ok: false, Msg: ""} +成功: type.Note +``` + +### /note/getNoteContent 获得笔记内容 +``` +参数: noteId +Method: GET +返回: +错误: {Ok: false, Msg: ""} +成功: type.NoteContent +``` + +### /note/addNote 添加笔记 +``` +参数: (注意首字大写) + NotebookId string 必传 + Title string 必传 + Tags []string 可选 + Content string 必传 + Abstract string 可选, 当是markdown笔记时必须传 + IsMarkdown bool 可选 + Files []type.NoteFiles 数组 可选 +Method: POST +返回: +错误: {Ok: false, Msg: ""} +成功: type.Note, 不包含Abstract, Content +``` + +** 关于笔记中的图片/附件** +客户端应该添加一个"图片/附件表"来存元数据, 图片应该要缓存到本地, 附件可在需要的时候再调用相应api获取. + +Content中的数据, 图片,附件在Leanote的链接是, 所以, 不管你在本地的笔记中是以什么形式来保存图片,附件的链接的,请addNote和updateNote时务必将链接修改成leanote服务器上的链接. +http://leanote.com/api/file/getImage?fileId=xx +单个附件: +http://leanote.com/api/file/getAttach?fileId=xx +所有附件: +http://leanote.com/api/file/getAllAttachs?noteId=xxx +``` + +**注意:** +addNote时必须要把Files, 和相关的图片/附件一起传到服务器中 +其中Files(文件的元数据)和其它字段以POST方式传出, 而真正数据则以http的multipart传入, 每个文件的name为"FileDatas[LocalFileId]" + +图片在笔记内的链接必须为: http://leanote.com/api/file/getImage?fileId=LocalFileId或FileId +附件如果插入到了笔记内容内, 其链接必须为: http://leanote.com/api/file/getAttach?fileId=LocalFileId或FileId +其中, fileId为文件在本地的LocalFileId或服务器上的FileId + +服务器端会生成FileId传给Client. Client在本地必须要建立LocalFileId与FileId的关联. + +如果笔记内容传了, 且笔记内有图片, 则必须要传Files 文件元数据, 因为Server端还要对内容内的图片, 附件链接进行修改, 可能你传过去的是LocalFileId, 服务器端会将LocalFileId替换成FileId存到数据库中. + +同样适用于 updateNote + +http://leanote.com 不绝对, 因为用户可以自建服务, 所以在开发时需要可配置 + +### /note/updateNote 更新笔记 +当更新了笔记某个属性时, 只要传某个属性就行, 其它不用传, 比如把笔记拉入了trash, 那么就传IsTrash: true +``` +参数: (注意首字大写) + NoteId string 必传 + Usn int 必传 + NotebookId string 可选 + Title string 可选 + Tags []string 可选 + Content string 可选 + Abstract string 可选, 当是markdown笔记时必须传 + IsMarkdown bool 可选 + IsTrash bool 是否是trash 可选 + Files []type.NoteFiles 数组 可选 +Method: POST +返回: +错误: {Ok: false, msg: ''} msg == 'conflict' 表示冲突 +成功: type.Note, 不包含Abstract和Content +``` + +### /note/deleteTrash 彻底删除笔记 +``` +参数: noteId, usn +Method: GET +返回: +错误: {Ok: false, msg: ''} msg == 'conflict' 表示冲突 +成功: type.UpdateRet +``` + +------- + +## Tag 标签 + +### /tag/getSyncTags 获取需要同步的标签 +``` +参数: afterUsn(int, 在此usn后的标签是需要同步的), maxEntry(int, 最大要同步的量) +Method: GET +返回: +错误: {Ok: false, Msg: ""} +成功: [type.Tag] 数组 +``` + +### /tag/addTag 添加标签 +``` +参数: tag(string) +Method: POST +返回: +错误: {Ok: false, Msg: ""} +成功: type.Tag +``` + +### /tag/deleteTag 删除标签 +``` +参数: tag(string) +Method: POST +返回: +错误: {Ok: false, Msg: ""} +成功: type.UpdateRet +``` + +### File 文件(获取图片, 附件) + +### /file/getImage 获取图片 +``` +参数: fileId +Method: GET +返回: +错误: 非二进制文件数据 +成功: 二进制文件 +``` + +### /file/getAttach 获取附件 +``` +参数: fileId +Method: GET +返回: +错误: 非二进制文件数据 +成功: 二进制文件 +``` + +### /file/getAllAttachs 获取所有附件 +``` +参数: noteId +Method: GET +返回: +错误: 非二进制文件数据 +成功: 二进制文件 +``` + +-------- + +## 数据类型 + +### type.User 用户信息 + +``` +User { + UserId string + Username string + Email string + Verified bool + Logo string +} +``` + +### type.Notebook 笔记本 + +``` +Notebook { + NotebookId + UserId + ParentNotebookId // 上级 + Seq int // 排序 + Title string + IsBlog bool + CreatedTime time.Time + UpdatedTime time.Time + + // 更新序号 + Usn int // UpdateSequenceNum +} +``` + +### type.Note 笔记 +``` +Note { + NoteId string + NotebookId string + UserId string + Title string + Tags []string + Content string + IsMarkdown bool + IsBlog bool + IsTrash bool + Files []NoteFile // 图片, 附件 + CreatedTime time.Time + UpdatedTime time.Time + PublicTime time.Time + + // 更新序号 + Usn int +} +``` + +### type.NoteContent 笔记内容 +``` +NoteContent { + NoteId string + UserId string + Content string +} +``` + +### type.NoteFile 笔记文件(图片,附件) +``` +NoteFile { + FileId string // 服务器端Id + LocalFileId string // 客户端Id + Type string // images/png, doc, xls, 根据fileName确定 + Title string + HasBody bool // 传过来的值是否要更新内容, 如果有true, 则必须传文件 + IsAttach bool // 是否是附件, 不是附件就是图片 +} +``` + +### type.Tag 标签 +``` +Tag { + TagId string + UserId string + Tag string + CreatedTime + UpdatedTime + IsDeleted bool // 删除位 + // 更新序号 + Usn +} +``` + +### type.UpdateRe 更新后返回的值, 包含Usn +``` +ReUpdate { + Ok bool + Msg string + + // 更新序号 + Usn int +} +``` diff --git a/app/controllers/api/API设计-v0.1.md b/app/controllers/api/API设计-v0.1.md new file mode 100644 index 0000000..56e84dd --- /dev/null +++ b/app/controllers/api/API设计-v0.1.md @@ -0,0 +1,47 @@ +# API设计 + +By life (life@leanote.com) + +## api url + +所有api的url前面带/api/, 如: + +`/api/user/info?userId=xxxx&token=xxxx` + +## 文件目录结构 +* 所有API的Controller都在app/api文件夹下 +* 文件命名: Api功能Controller.go, 如ApiUserController.go +* 结构体命名为 Api功能, 如ApiUser +* API公用Controller: ApiBaseController +* init.go 注入service和定义拦截器 + +## 流程 +用户登录后返回一个token, 以后所有的请求都携带该token. +在init.go中的拦截器会得到token并调用sessionService判断是否登录了 + +## 返回值结构 +* 全部返回JSON, JSON, 除二进制文件(图片, 附件外), 如果返回其它非JSON格式的值, 肯定是出错了 +* 错误信息全部返回 {Ok: false, Msg: ""} +* 正确信息全部返回真实的返回数据, 如返回笔记: +``` +{ + "NoteId": "54bdc7e305fcd13ea3000000", + "NotebookId": "54bdc65599c37b0da9000003", + "UserId": "54bdc65599c37b0da9000002", + "Title": "asdfads", + "Desc": "", + "Tags": null, + "Abstract": "", + "Content": "", + "IsMarkdown": false, + "IsBlog": false, + "IsTrash": true, + "IsDeleted": false, + "Usn": 15, + "Files": [], + "CreatedTime": "2015-01-20T11:13:41.34+08:00", + "UpdatedTime": "2015-01-20T11:13:41.34+08:00", + "PublicTime": "0001-01-01T00:00:00Z" +} +``` +* 时间类型全是返回如 "2015-01-20T11:13:41.34+08:00" 的格式, (因为golang的time转成Json就是这样, 历史原因) \ No newline at end of file diff --git a/app/controllers/api/ApiAuthController.go b/app/controllers/api/ApiAuthController.go new file mode 100644 index 0000000..e4f35aa --- /dev/null +++ b/app/controllers/api/ApiAuthController.go @@ -0,0 +1,69 @@ +package api + +import ( + "github.com/leanote/leanote/app/info" + . "github.com/leanote/leanote/app/lea" + "github.com/revel/revel" + "gopkg.in/mgo.v2/bson" + // "strconv" +) + +// 用户登录后生成一个token, 将这个token保存到session中 +// 以后每次的请求必须带这个token, 并从session中获取userId + +// 用户登录/注销/找回密码 + +type ApiAuth struct { + ApiBaseContrller +} + +// 登录 +// [ok] +// 成功返回 {Ok: true, Item: token } +// 失败返回 {Ok: false, Msg: ""} +func (c ApiAuth) Login(email, pwd string) revel.Result { + var msg = "" + + userInfo := authService.Login(email, pwd) + if userInfo.Email != "" { + token := bson.NewObjectId().Hex() + sessionService.SetUserId(token, userInfo.UserId.Hex()) + return c.RenderJson(info.AuthOk{Ok: true, Token: token, UserId: userInfo.UserId, Email: userInfo.Email, Username: userInfo.Username}) + } else { + // 登录错误, 则错误次数++ + msg = "wrongUsernameOrPassword" + } + return c.RenderJson(info.ApiRe{Ok: false, Msg: c.Message(msg)}) +} + +// 注销 +// [Ok] +func (c ApiAuth) Logout() revel.Result { + token := c.getToken() + sessionService.Clear(token) + re := info.NewApiRe() + re.Ok = true + return c.RenderJson(re) +} + +// 注册 +// [Ok] +// 成功后并不返回用户ID, 需要用户重新登录 +func (c ApiAuth) Register(email, pwd string) revel.Result { + re := info.NewApiRe() + if !configService.IsOpenRegister() { + re.Msg = "notOpenRegister" // 未开放注册 + return c.RenderJson(re) + } + + if re.Ok, re.Msg = Vd("email", email); !re.Ok { + return c.RenderJson(re) + } + if re.Ok, re.Msg = Vd("password", pwd); !re.Ok { + return c.RenderJson(re) + } + + // 注册 + re.Ok, re.Msg = authService.Register(email, pwd, "") + return c.RenderJson(re) +} \ No newline at end of file diff --git a/app/controllers/api/ApiBaseController.go b/app/controllers/api/ApiBaseController.go new file mode 100644 index 0000000..4c70c06 --- /dev/null +++ b/app/controllers/api/ApiBaseController.go @@ -0,0 +1,181 @@ +package api + +import ( + "github.com/revel/revel" + "gopkg.in/mgo.v2/bson" + // "encoding/json" + . "github.com/leanote/leanote/app/lea" + "github.com/leanote/leanote/app/controllers" + "github.com/leanote/leanote/app/info" + "os" +// "fmt" + "io/ioutil" + // "fmt" + // "math" + // "strconv" + "strings" +) + +// 公用Controller, 其它Controller继承它 +type ApiBaseContrller struct { + controllers.BaseController // 不能用*BaseController +} + +// 得到token, 这个token是在AuthInterceptor设到Session中的 +func (c ApiBaseContrller) getToken() string { + return c.Session["_token"] +} + +// userId +// _userId是在AuthInterceptor设置的 +func (c ApiBaseContrller) getUserId() string { + return c.Session["_userId"] +} + +// 得到用户信息 +func (c ApiBaseContrller) getUserInfo() info.User { + userId := c.Session["_userId"] + if userId == "" { + return info.User{} + } + return userService.GetUserInfo(userId) +} + +// 上传附件 +func (c ApiBaseContrller) uploadAttach(name string, noteId string) (ok bool, msg string, id string) { + userId := c.getUserId(); + + // 判断是否有权限为笔记添加附件 + // 如果笔记还没有添加是不是会有问题 + /* + if !shareService.HasUpdateNotePerm(noteId, userId) { + return + } + */ + + file, handel, err := c.Request.FormFile(name) + if err != nil { + return + } + defer file.Close() + + data, err := ioutil.ReadAll(file) + if err != nil { + return + } + // > 5M? + maxFileSize := configService.GetUploadSize("uploadAttachSize"); + if maxFileSize <= 0 { + maxFileSize = 1000 + } + if(float64(len(data)) > maxFileSize * float64(1024*1024)) { + msg = "fileIsTooLarge" + return + } + + // 生成上传路径 + filePath := "files/" + userId + "/attachs" + dir := revel.BasePath + "/" + filePath + err = os.MkdirAll(dir, 0755) + if err != nil { + return + } + // 生成新的文件名 + filename := handel.Filename + _, ext := SplitFilename(filename) // .doc + filename = NewGuid() + ext + toPath := dir + "/" + filename; + err = ioutil.WriteFile(toPath, data, 0777) + if err != nil { + return + } + + // add File to db + fileType := "" + if ext != "" { + fileType = strings.ToLower(ext[1:]) + } + filesize := GetFilesize(toPath) + fileInfo := info.Attach{AttachId: bson.NewObjectId(), + Name: filename, + Title: handel.Filename, + NoteId: bson.ObjectIdHex(noteId), + UploadUserId: bson.ObjectIdHex(userId), + Path: filePath + "/" + filename, + Type: fileType, + Size: filesize} + + ok, msg = attachService.AddAttach(fileInfo, true) + if !ok { + return + } + + id = fileInfo.AttachId.Hex() + return +} + +// 上传图片 +func (c ApiBaseContrller) upload(name string, noteId string, isAttach bool) (ok bool, msg string, id string) { + if isAttach { + return c.uploadAttach(name, noteId) + } + file, handel, err := c.Request.FormFile(name) + if err != nil { + return + } + defer file.Close() + // 生成上传路径 + fileUrlPath := "files/" + c.getUserId() + "/images" + dir := revel.BasePath + "/" + fileUrlPath + err = os.MkdirAll(dir, 0755) + if err != nil { + return + } + // 生成新的文件名 + filename := handel.Filename + _, ext := SplitFilename(filename) + if(ext != ".gif" && ext != ".jpg" && ext != ".png" && ext != ".bmp" && ext != ".jpeg") { + msg = "notImage" + return + } + + filename = NewGuid() + ext + data, err := ioutil.ReadAll(file) + if err != nil { + return + } + + maxFileSize := configService.GetUploadSize("uploadImageSize"); + if maxFileSize <= 0 { + maxFileSize = 1000 + } + + // > 2M? + if(float64(len(data)) > maxFileSize * float64(1024*1024)) { + msg = "fileIsTooLarge" + return + } + + toPath := dir + "/" + filename; + err = ioutil.WriteFile(toPath, data, 0777) + if err != nil { + return + } + // 改变成gif图片 + _, toPathGif := TransToGif(toPath, 0, true) + filename = GetFilename(toPathGif) + filesize := GetFilesize(toPathGif) + fileUrlPath += "/" + filename + + // File + fileInfo := info.File{FileId: bson.NewObjectId(), + Name: filename, + Title: handel.Filename, + Path: fileUrlPath, + Size: filesize} + ok, msg = fileService.AddImage(fileInfo, "", c.getUserId(), true) + if ok { + id = fileInfo.FileId.Hex() + } + return +} diff --git a/app/controllers/api/ApiFileController.go b/app/controllers/api/ApiFileController.go new file mode 100644 index 0000000..530e9fe --- /dev/null +++ b/app/controllers/api/ApiFileController.go @@ -0,0 +1,164 @@ +package api + +import ( + "github.com/revel/revel" + // "encoding/json" +// . "github.com/leanote/leanote/app/lea" +// "gopkg.in/mgo.v2/bson" + // "github.com/leanote/leanote/app/lea/netutil" +// "github.com/leanote/leanote/app/info" +// "io/ioutil" + "os" + // "strconv" + "io" + "time" + "strings" + "archive/tar" + "compress/gzip" +) + +// 文件操作, 图片, 头像上传, 输出 + +type ApiFile struct { + ApiBaseContrller +} + +/* +// 协作时复制图片到owner +func (c ApiFile) CopyImage(userId, fileId, toUserId string) revel.Result { + re := info.NewRe() + + re.Ok, re.Id = fileService.CopyImage(userId, fileId, toUserId) + + return c.RenderJson(re) +} + +// get all images by userId with page +func (c ApiFile) GetImages(albumId, key string, page int) revel.Result { + imagesPage := fileService.ListImagesWithPage(c.getUserId(), albumId, key, page, 12) + re := info.NewRe() + re.Ok = true + re.Item = imagesPage + return c.RenderJson(re) +} + +func (c ApiFile) UpdateImageTitle(fileId, title string) revel.Result { + re := info.NewRe() + re.Ok = fileService.UpdateImageTitle(c.getUserId(), fileId, title) + return c.RenderJson(re) +} + +func (c ApiFile) DeleteImage(fileId string) revel.Result { + re := info.NewRe() + re.Ok, re.Msg = fileService.DeleteImage(c.getUserId(), fileId) + return c.RenderJson(re) +} + +*/ + +//----------- + +// 输出image +// [OK] +func (c ApiFile) GetImage(fileId string) revel.Result { + path := fileService.GetFile(c.getUserId(), fileId) // 得到路径 + if path == "" { + return c.RenderText("") + } + fn := revel.BasePath + "/" + strings.TrimLeft(path, "/") + file, _ := os.Open(fn) + return c.RenderFile(file, revel.Inline) // revel.Attachment +} + +// 下载附件 +// [OK] +func (c ApiFile) GetAttach(fileId string) revel.Result { + attach := attachService.GetAttach(fileId, c.getUserId()); // 得到路径 + path := attach.Path + if path == "" { + return c.RenderText("No Such File") + } + fn := revel.BasePath + "/" + strings.TrimLeft(path, "/") + file, _ := os.Open(fn) + return c.RenderBinary(file, attach.Title, revel.Attachment, time.Now()) // revel.Attachment +} + +// 下载所有附件 +// [OK] +func (c ApiFile) GetAllAttachs(noteId string) revel.Result { + note := noteService.GetNoteById(noteId) + if note.NoteId == "" { + return c.RenderText("") + } + // 得到文件列表 + attachs := attachService.ListAttachs(noteId, c.getUserId()) + if attachs == nil || len(attachs) == 0 { + return c.RenderText("") + } + + /* + dir := revel.BasePath + "/files/tmp" + err := os.MkdirAll(dir, 0755) + if err != nil { + return c.RenderText("") + } + */ + + filename := note.Title + ".tar.gz" + if note.Title == "" { + filename = "all.tar.gz" + } + + // file write + fw, err := os.Create(revel.BasePath + "/files/" + filename) + if err != nil { + return c.RenderText("") + } + // defer fw.Close() // 不需要关闭, 还要读取给用户下载 + + // gzip write + gw := gzip.NewWriter(fw) + defer gw.Close() + + // tar write + tw := tar.NewWriter(gw) + defer tw.Close() + + // 遍历文件列表 + for _, attach := range attachs { + fn := revel.BasePath + "/" + strings.TrimLeft(attach.Path, "/") + fr, err := os.Open(fn) + fileInfo, _ := fr.Stat() + if err != nil { + return c.RenderText("") + } + defer fr.Close() + + // 信息头 + h := new(tar.Header) + h.Name = attach.Title + h.Size = fileInfo.Size() + h.Mode = int64(fileInfo.Mode()) + h.ModTime = fileInfo.ModTime() + + // 写信息头 + err = tw.WriteHeader(h) + if err != nil { + panic(err) + } + + // 写文件 + _, err = io.Copy(tw, fr) + if err != nil { + panic(err) + } + } // for + +// tw.Close() +// gw.Close() +// fw.Close() +// file, _ := os.Open(dir + "/" + filename) + // fw.Seek(0, 0) + return c.RenderBinary(fw, filename, revel.Attachment, time.Now()) // revel.Attachment +} + diff --git a/app/controllers/api/ApiNoteController.go b/app/controllers/api/ApiNoteController.go new file mode 100644 index 0000000..700d250 --- /dev/null +++ b/app/controllers/api/ApiNoteController.go @@ -0,0 +1,573 @@ +package api + +import ( + "github.com/revel/revel" + // "encoding/json" + "github.com/leanote/leanote/app/info" + . "github.com/leanote/leanote/app/lea" + "gopkg.in/mgo.v2/bson" + "regexp" + "strings" + "time" + // "github.com/leanote/leanote/app/types" + // "io/ioutil" + // "fmt" + // "bytes" + // "os" +) + +// 笔记API + +type ApiNote struct { + ApiBaseContrller +} + +// 获取同步的笔记 +// > afterUsn的笔记 +// 无Desc, Abstract, 有Files +/* + { + "NoteId": "55195fa199c37b79be000005", + "NotebookId": "55195fa199c37b79be000002", + "UserId": "55195fa199c37b79be000001", + "Title": "Leanote语法Leanote语法Leanote语法Leanote语法Leanote语法", + "Desc": "", + "Tags": null, + "Abstract": "", + "Content": "", + "IsMarkdown": true, + "IsBlog": false, + "IsTrash": false, + "Usn": 5, + "Files": [], + "CreatedTime": "2015-03-30T22:37:21.695+08:00", + "UpdatedTime": "2015-03-30T22:37:21.724+08:00", + "PublicTime": "2015-03-30T22:37:21.695+08:00" + } +*/ +func (c ApiNote) GetSyncNotes(afterUsn, maxEntry int) revel.Result { + if maxEntry == 0 { + maxEntry = 100 + } + notes := noteService.GetSyncNotes(c.getUserId(), afterUsn, maxEntry) + return c.RenderJson(notes) +} + +// 得到笔记本下的笔记 +// [OK] +func (c ApiNote) GetNotes(notebookId string) revel.Result { + if notebookId != "" && !bson.IsObjectIdHex(notebookId) { + re := info.NewApiRe() + re.Msg = "notebookIdInvalid" + return c.RenderJson(re) + } + _, notes := noteService.ListNotes(c.getUserId(), notebookId, false, c.GetPage(), pageSize, defaultSortField, false, false) + return c.RenderJson(noteService.ToApiNotes(notes)) +} + +// 得到trash +// [OK] +func (c ApiNote) GetTrashNotes() revel.Result { + _, notes := noteService.ListNotes(c.getUserId(), "", true, c.GetPage(), pageSize, defaultSortField, false, false) + return c.RenderJson(noteService.ToApiNotes(notes)) +} + +// get Note +// [OK] +/* +{ + "NoteId": "550c0bee2ec82a2eb5000000", + "NotebookId": "54a1676399c37b1c77000004", + "UserId": "54a1676399c37b1c77000002", + "Title": "asdfadsf--=", + "Desc": "", + "Tags": [ + "" + ], + "Abstract": "", + "Content": "", + "IsMarkdown": false, + "IsBlog": false, + "IsTrash": false, + "Usn": 8, + "Files": [ + { + "FileId": "551975d599c37b970f000002", + "LocalFileId": "", + "Type": "", + "Title": "", + "HasBody": false, + "IsAttach": false + }, + { + "FileId": "551975de99c37b970f000003", + "LocalFileId": "", + "Type": "doc", + "Title": "李铁-简历-ali-print-en.doc", + "HasBody": false, + "IsAttach": true + }, + { + "FileId": "551975de99c37b970f000004", + "LocalFileId": "", + "Type": "doc", + "Title": "李铁-简历-ali-print.doc", + "HasBody": false, + "IsAttach": true + } + ], + "CreatedTime": "2015-03-20T20:00:52.463+08:00", + "UpdatedTime": "2015-03-31T00:12:44.967+08:00", + "PublicTime": "2015-03-20T20:00:52.463+08:00" +} +*/ +func (c ApiNote) GetNote(noteId string) revel.Result { + if !bson.IsObjectIdHex(noteId) { + re := info.NewApiRe() + re.Msg = "noteIdInvalid" + return c.RenderJson(re) + } + + note := noteService.GetNote(noteId, c.getUserId()) + if note.NoteId == "" { + re := info.NewApiRe() + re.Msg = "notExists" + return c.RenderJson(re) + } + apiNotes := noteService.ToApiNotes([]info.Note{note}) + return c.RenderJson(apiNotes[0]) +} + +// 得到note和内容 +// [OK] +func (c ApiNote) GetNoteAndContent(noteId string) revel.Result { + noteAndContent := noteService.GetNoteAndContent(noteId, c.getUserId()) + + apiNotes := noteService.ToApiNotes([]info.Note{noteAndContent.Note}) + apiNote := apiNotes[0] + apiNote.Content = noteAndContent.Content + return c.RenderJson(apiNote) +} + +// 处理笔记内容数据 http://leanote.com/file/outputImage -> https://leanote.com/api/file/getImage +// 图片, 附件都替换 +func (c ApiNote) fixContent(content string) string { + // TODO, 这个url需要从config中取 +// baseUrl := "http://leanote.com" + baseUrl := configService.GetSiteUrl() +// baseUrl := "http://localhost:9000" + + patterns := []map[string]string{ + map[string]string{"src": "src", "middle": "/file/outputImage", "param": "fileId", "to": "getImage?fileId="}, + map[string]string{"src": "href", "middle": "/attach/download", "param": "attachId", "to": "getAttach?fileId="}, + map[string]string{"src": "href", "middle": "/attach/downloadAll", "param": "noteId", "to": "getAllAttachs?noteId="}, + } + + for _, eachPattern := range patterns { + + // src="http://leanote.com/file/outputImage?fileId=5503537b38f4111dcb0000d1" + // href="http://leanote.com/attach/download?attachId=5504243a38f4111dcb00017d" + // href="http://leanote.com/attach/downloadAll?noteId=55041b6a38f4111dcb000159" + + regImage, _ := regexp.Compile(eachPattern["src"] + `=('|")`+ baseUrl + eachPattern["middle"] + `\?` + eachPattern["param"] + `=([a-z0-9A-Z]{24})("|')`) + findsImage := regImage.FindAllStringSubmatch(content, -1) // 查找所有的 + + // [[src='http://leanote.com/file/outputImage?fileId=54672e8d38f411286b000069" ' 54672e8d38f411286b000069 "] [src="http://leanote.com/file/outputImage?fileId=54672e8d38f411286b000069" " 54672e8d38f411286b000069 "] [src="http://leanote.com/file/outputImage?fileId=54672e8d38f411286b000069" " 54672e8d38f411286b000069 "] [src="http://leanote.com/file/outputImage?fileId=54672e8d38f411286b000069" " 54672e8d38f411286b000069 "]] + for _, eachFind := range findsImage { + // [src='http://leanote.com/file/outputImage?fileId=54672e8d38f411286b000069" ' 54672e8d38f411286b000069 "] + if len(eachFind) == 4 { + content = strings.Replace(content, + eachFind[0], + eachPattern["src"] + "=\"" + baseUrl + "/api/file/" + eachPattern["to"] + eachFind[2] + "\"", + 1); + } + } + + // markdown处理 + //  + // [selection 2.html](http://leanote.com/attach/download?attachId=5504262638f4111dcb00017f) + // [all.tar.gz](http://leanote.com/attach/downloadAll?noteId=5503b57d59f81b4eb4000000) + + pre := "!" // 默认图片 + if eachPattern["src"] == "href" { // 是attach + pre = "" + } + + regImageMarkdown, _ := regexp.Compile(pre + `\[(.*?)\]\(`+ baseUrl + eachPattern["middle"] + `\?` + eachPattern["param"] + `=([a-z0-9A-Z]{24})\)`) + findsImageMarkdown := regImageMarkdown.FindAllStringSubmatch(content, -1) // 查找所有的 + // [[ 5503537b38f4111dcb0000d1] [ 5503537b38f4111dcb0000d1]] + for _, eachFind := range findsImageMarkdown { + // [ 你好啊, 我很好, 为什么? 5503537b38f4111dcb0000d1] + if len(eachFind) == 3 { + content = strings.Replace(content, eachFind[0], pre + "[" + eachFind[1] + "](" + baseUrl + "/api/file/" + eachPattern["to"] + eachFind[2] + ")", 1); + } + } + } + + return content +} + +// content里的image, attach链接是 +// https://leanote.com/api/file/getImage?fileId=xx +// https://leanote.com/api/file/getAttach?fileId=xx +// 将fileId=映射成ServerFileId, 这里的fileId可能是本地的FileId +func (c ApiNote) fixPostNotecontent(noteOrContent *info.ApiNote) { + if noteOrContent.Content == "" { + return + } + files := noteOrContent.Files + if files != nil && len(files) > 0 { + for _, file := range files { + if file.LocalFileId != "" { + noteOrContent.Content = strings.Replace(noteOrContent.Content, "fileId=" + file.LocalFileId, "fileId=" + file.FileId, -1) + } + } + } +} + +// 得到内容 +// [OK] +func (c ApiNote) GetNoteContent(noteId string) revel.Result { + // re := info.NewRe() + noteContent := noteService.GetNoteContent(noteId, c.getUserId()) + if noteContent.Content != "" { + noteContent.Content = c.fixContent(noteContent.Content) + } + + apiNoteContent := info.ApiNoteContent{ + NoteId: noteContent.NoteId, + UserId: noteContent.UserId, + Content: noteContent.Content, + } + + // re.Item = noteContent + return c.RenderJson(apiNoteContent) +} + +// 添加笔记 +// [OK] +func (c ApiNote) AddNote(noteOrContent info.ApiNote) revel.Result { + userId := bson.ObjectIdHex(c.getUserId()) + re := info.NewRe() + myUserId := userId + // 为共享新建? + /* + if noteOrContent.FromUserId != "" { + userId = bson.ObjectIdHex(noteOrContent.FromUserId) + } + */ + // Log(noteOrContent.Title) + // LogJ(noteOrContent) + /* + LogJ(c.Params) + for name, _ := range c.Params.Files { + Log(name) + file, _, _ := c.Request.FormFile(name) + LogJ(file) + } + */ + // return c.RenderJson(re) + if noteOrContent.NotebookId == "" || !bson.IsObjectIdHex(noteOrContent.NotebookId) { + re.Msg = "notebookIdNotExists" + return c.RenderJson(re) + } + + noteId := bson.NewObjectId() + // TODO 先上传图片/附件, 如果不成功, 则返回false + // + attachNum := 0 + if noteOrContent.Files != nil && len(noteOrContent.Files) > 0 { + for i, file := range noteOrContent.Files { + if file.HasBody { + if file.LocalFileId != "" { + // FileDatas[54c7ae27d98d0329dd000000] + ok, msg, fileId := c.upload("FileDatas["+file.LocalFileId+"]", noteId.Hex(), file.IsAttach) + + if !ok { + re.Ok = false + if msg != "" { + Log(msg) + Log(file.LocalFileId) + re.Msg = "fileUploadError" + } + // 报不是图片的错误没关系, 证明客户端传来非图片的数据 + if msg != "notImage" { + return c.RenderJson(re) + } + } else { + // 建立映射 + file.FileId = fileId + noteOrContent.Files[i] = file + + if file.IsAttach { + attachNum++ + } + } + } else { + return c.RenderJson(re) + } + } + } + } + + c.fixPostNotecontent(¬eOrContent) + +// Log("Add") +// LogJ(noteOrContent) + + // return c.RenderJson(re) + + note := info.Note{UserId: userId, + NoteId: noteId, + NotebookId: bson.ObjectIdHex(noteOrContent.NotebookId), + Title: noteOrContent.Title, + Tags: noteOrContent.Tags, + Desc: noteOrContent.Desc, +// ImgSrc: noteOrContent.ImgSrc, + IsBlog: noteOrContent.IsBlog, + IsMarkdown: noteOrContent.IsMarkdown, + AttachNum: attachNum, + } + noteContent := info.NoteContent{NoteId: note.NoteId, + UserId: userId, + IsBlog: note.IsBlog, + Content: noteOrContent.Content, + Abstract: noteOrContent.Abstract} + + // 通过内容得到Desc, abstract + note.Desc = SubStringHTMLToRaw(noteContent.Content, 50) + noteContent.Abstract = SubStringHTML(noteContent.Content, 200, "") + + note = noteService.AddNoteAndContentApi(note, noteContent, myUserId) + + if note.NoteId == "" { + re.Ok = false + return c.RenderJson(re) + } + + // 添加需要返回的 + noteOrContent.NoteId = note.NoteId.Hex() + noteOrContent.Usn = note.Usn + noteOrContent.CreatedTime = note.CreatedTime + noteOrContent.UpdatedTime = note.UpdatedTime + noteOrContent.UserId = c.getUserId() + noteOrContent.IsMarkdown = note.IsMarkdown + // 删除一些不要返回的 + noteOrContent.Content = "" + noteOrContent.Abstract = "" + // apiNote := info.NoteToApiNote(note, noteOrContent.Files) + return c.RenderJson(noteOrContent) +} + +// 更新笔记 +// [OK] +func (c ApiNote) UpdateNote(noteOrContent info.ApiNote) revel.Result { + re := info.NewReUpdate() + + noteUpdate := bson.M{} + needUpdateNote := false + + noteId := noteOrContent.NoteId + + if noteOrContent.NoteId == "" { + re.Msg = "noteIdNotExists" + return c.RenderJson(re) + } + + if noteOrContent.Usn <= 0 { + re.Msg = "usnNotExists" + return c.RenderJson(re) + } + +// Log("_____________") +// LogJ(noteOrContent) + /* + LogJ(c.Params.Files) + LogJ(c.Request.Header) + LogJ(c.Params.Values) + */ + + // 先判断USN的问题, 因为很可能添加完附件后, 会有USN冲突, 这时附件就添错了 + userId := c.getUserId() + note := noteService.GetNote(noteId, userId) + if note.NoteId == "" { + re.Msg = "notExists" + return c.RenderJson(re) + } + if note.Usn != noteOrContent.Usn { + re.Msg = "conflict" + Log("conflict") + return c.RenderJson(re) + } + + // 如果传了files + // TODO 测试 + /* + for key, v := range c.Params.Values { + Log(key) + Log(v) + } + */ +// Log(c.Has("Files[0]")) + if c.Has("Files[0][LocalFileId]") { +// LogJ(c.Params.Files) + if noteOrContent.Files != nil && len(noteOrContent.Files) > 0 { + for i, file := range noteOrContent.Files { + if file.HasBody { + if file.LocalFileId != "" { + // FileDatas[54c7ae27d98d0329dd000000] + ok, msg, fileId := c.upload("FileDatas["+file.LocalFileId+"]", noteId, file.IsAttach) + if !ok { + Log("upload file error") + re.Ok = false + if msg == "" { + re.Msg = "fileUploadError" + } else { + re.Msg = msg + } + return c.RenderJson(re) + } else { + // 建立映射 + file.FileId = fileId + noteOrContent.Files[i] = file + } + } else { + return c.RenderJson(re) + } + } + } + } + +// Log("after upload") +// LogJ(noteOrContent.Files) + } + + // 移到外面来, 删除最后一个file时也要处理, 不然总删不掉 + // 附件问题, 根据Files, 有些要删除的, 只留下这些 + attachService.UpdateOrDeleteAttachApi(noteId, userId, noteOrContent.Files) + + // Desc前台传来 + if c.Has("Desc") { + needUpdateNote = true + noteUpdate["Desc"] = noteOrContent.Desc + } + /* + if c.Has("ImgSrc") { + needUpdateNote = true + noteUpdate["ImgSrc"] = noteOrContent.ImgSrc + } + */ + if c.Has("Title") { + needUpdateNote = true + noteUpdate["Title"] = noteOrContent.Title + } + if c.Has("IsTrash") { + needUpdateNote = true + noteUpdate["IsTrash"] = noteOrContent.IsTrash + } + + // 是否是博客 + if c.Has("IsBlog") { + needUpdateNote = true + noteUpdate["IsBlog"] = noteOrContent.IsBlog + } + + /* + Log(c.Has("tags[0]")) + Log(c.Has("Tags[]")) + for key, v := range c.Params.Values { + Log(key) + Log(v) + } + */ + + if c.Has("Tags[0]") { + needUpdateNote = true + noteUpdate["Tags"] = noteOrContent.Tags + } + + if c.Has("NotebookId") { + if bson.IsObjectIdHex(noteOrContent.NotebookId) { + needUpdateNote = true + noteUpdate["NotebookId"] = bson.ObjectIdHex(noteOrContent.NotebookId) + } + } + + if c.Has("Content") { + // 通过内容得到Desc, abstract + noteUpdate["Desc"] = SubStringHTMLToRaw(noteOrContent.Content, 50) + } + + afterNoteUsn := 0 + noteOk := false + noteMsg := "" + if needUpdateNote { + noteOk, noteMsg, afterNoteUsn = noteService.UpdateNote(c.getUserId(), noteOrContent.NoteId, noteUpdate, noteOrContent.Usn) + if !noteOk { + re.Ok = false + re.Msg = noteMsg + return c.RenderJson(re) + } + } + + //------------- + afterContentUsn := 0 + contentOk := false + contentMsg := "" + if c.Has("Content") { + // 把fileId替换下 + c.fixPostNotecontent(¬eOrContent) + // 如果传了Abstract就用之 + if noteOrContent.Abstract == "" { + noteOrContent.Abstract = SubStringHTML(noteOrContent.Content, 200, "") + } +// Log("--------> afte fixed") +// Log(noteOrContent.Content) + contentOk, contentMsg, afterContentUsn = noteService.UpdateNoteContent(c.getUserId(), + noteOrContent.NoteId, noteOrContent.Content, noteOrContent.Abstract, needUpdateNote, noteOrContent.Usn) + } + + if needUpdateNote { + re.Ok = noteOk + re.Msg = noteMsg + re.Usn = afterNoteUsn + } else { + re.Ok = contentOk + re.Msg = contentMsg + re.Usn = afterContentUsn + } + + if !re.Ok { + return c.RenderJson(re) + } + + noteOrContent.Content = "" + noteOrContent.Usn = re.Usn + noteOrContent.UpdatedTime = time.Now() + +// Log("after upload") +// LogJ(noteOrContent.Files) + noteOrContent.UserId = c.getUserId() + + return c.RenderJson(noteOrContent) +} + +// 删除trash +func (c ApiNote) DeleteTrash(noteId string, usn int) revel.Result { + re := info.NewReUpdate() + re.Ok, re.Msg, re.Usn = trashService.DeleteTrashApi(noteId, c.getUserId(), usn) + return c.RenderJson(re) +} + +// 得到历史列表 +/* +func (c ApiNote) GetHistories(noteId string) revel.Result { + re := info.NewRe() + histories := noteContentHistoryService.ListHistories(noteId, c.getUserId()) + if len(histories) > 0 { + re.Ok = true + re.Item = histories + } + return c.RenderJson(re) +} +*/ \ No newline at end of file diff --git a/app/controllers/api/ApiNotebookController.go b/app/controllers/api/ApiNotebookController.go new file mode 100644 index 0000000..53e8c67 --- /dev/null +++ b/app/controllers/api/ApiNotebookController.go @@ -0,0 +1,106 @@ +package api + +import ( + "github.com/leanote/leanote/app/info" + "github.com/revel/revel" + "gopkg.in/mgo.v2/bson" + . "github.com/leanote/leanote/app/lea" + // "io/ioutil" +) + +// 笔记本API + +type ApiNotebook struct { + ApiBaseContrller +} + +// 从Notebook -> ApiNotebook +func (c ApiNotebook) fixNotebooks(notebooks []info.Notebook) []info.ApiNotebook { + if notebooks == nil { + return nil + } + apiNotebooks := make([]info.ApiNotebook, len(notebooks)) + for i, notebook := range notebooks { + apiNotebooks[i] = c.fixNotebook(¬ebook) + } + return apiNotebooks +} +func (c ApiNotebook) fixNotebook(notebook *info.Notebook) info.ApiNotebook { + if notebook == nil { + return info.ApiNotebook{} + } + return info.ApiNotebook{ + NotebookId : notebook.NotebookId, + UserId : notebook.UserId, + ParentNotebookId : notebook.ParentNotebookId, + Seq : notebook.Seq, + Title : notebook.Title, + UrlTitle : notebook.UrlTitle, + IsBlog : notebook.IsBlog, + CreatedTime : notebook.CreatedTime, + UpdatedTime : notebook.UpdatedTime, + Usn: notebook.Usn, + IsDeleted: notebook.IsDeleted, + } +} + +// 获取同步的笔记本 +// [OK] +// > afterUsn的笔记 +// 返回 {ChunkHighUsn: 本下最大的usn, 借此可以知道是否还有, Notebooks: []} +func (c ApiNotebook) GetSyncNotebooks(afterUsn, maxEntry int) revel.Result { + if maxEntry == 0 { + maxEntry = 100 + } + notebooks := notebookService.GeSyncNotebooks(c.getUserId(), afterUsn, maxEntry) + return c.RenderJson(c.fixNotebooks(notebooks)) +} + +// 得到用户的所有笔记本 +// [OK] +// info.SubNotebooks +func (c ApiNotebook) GetNotebooks() revel.Result { + notebooks := notebookService.GeSyncNotebooks(c.getUserId(), 0, 99999) + return c.RenderJson(c.fixNotebooks(notebooks)) +} + +// 添加notebook +// [OK] +func (c ApiNotebook) AddNotebook(title, parentNotebookId string, seq int) revel.Result { + notebook := info.Notebook{NotebookId: bson.NewObjectId(), + Title: title, + Seq: seq, + UserId: bson.ObjectIdHex(c.getUserId())} + if parentNotebookId != "" && bson.IsObjectIdHex(parentNotebookId) { + notebook.ParentNotebookId = bson.ObjectIdHex(parentNotebookId) + } + re := info.NewRe() + re.Ok, notebook = notebookService.AddNotebook(notebook) + if !re.Ok { + return c.RenderJson(re) + } + return c.RenderJson(c.fixNotebook(¬ebook)) +} + +// 修改笔记 +// [OK] +func (c ApiNotebook) UpdateNotebook(notebookId, title, parentNotebookId string, seq, usn int) revel.Result { + re := info.NewApiRe() + + ok, msg, notebook := notebookService.UpdateNotebookApi(c.getUserId(), notebookId, title, parentNotebookId, seq, usn) + if !ok { + re.Ok = false + re.Msg = msg + return c.RenderJson(re) + } + LogJ(notebook) + return c.RenderJson(c.fixNotebook(¬ebook)) +} + +// 删除笔记本 +// [OK] +func (c ApiNotebook) DeleteNotebook(notebookId string, usn int) revel.Result { + re := info.NewApiRe() + re.Ok, re.Msg = notebookService.DeleteNotebookForce(c.getUserId(), notebookId, usn) + return c.RenderJson(re) +} diff --git a/app/controllers/api/ApiTagController.go b/app/controllers/api/ApiTagController.go new file mode 100644 index 0000000..5b2c58f --- /dev/null +++ b/app/controllers/api/ApiTagController.go @@ -0,0 +1,56 @@ +package api + +import ( + "github.com/leanote/leanote/app/info" + "github.com/revel/revel" +// "gopkg.in/mgo.v2/bson" + // . "github.com/leanote/leanote/app/lea" + // "io/ioutil" +) + +// 标签API + +type ApiTag struct { + ApiBaseContrller +} + +// 获取同步的标签 +// [OK] +// > afterUsn的笔记 +// 返回 {ChunkHighUsn: 本下最大的usn, 借此可以知道是否还有, Notebooks: []} +func (c ApiTag) GetSyncTags(afterUsn, maxEntry int) revel.Result { + if maxEntry == 0 { + maxEntry = 100 + } + tags := tagService.GeSyncTags(c.getUserId(), afterUsn, maxEntry) + return c.RenderJson(tags) +} + +// 添加Tag +// [OK] +// 不会产生冲突, 即使里面有 +// 返回 +/* +{ + "TagId": "551978dd99c37b9bc5000001", + "UserId": "54a1676399c37b1c77000002", + "Tag": "32", + "Usn": 25, + "Count": 1, + "CreatedTime": "2015-03-31T00:25:01.149312407+08:00", + "UpdatedTime": "2015-03-31T00:25:01.149312407+08:00", + "IsDeleted": false +} +*/ +func (c ApiTag) AddTag(tag string) revel.Result { + ret := tagService.AddOrUpdateTag(c.getUserId(), tag) + return c.RenderJson(ret) +} + +// 删除标签 +// [OK] +func (c ApiTag) DeleteTag(tag string, usn int) revel.Result { + re := info.NewReUpdate() + re.Ok, re.Msg, re.Usn = tagService.DeleteTagApi(c.getUserId(), tag, usn) + return c.RenderJson(re) +} \ No newline at end of file diff --git a/app/controllers/api/ApiUserController.go b/app/controllers/api/ApiUserController.go new file mode 100644 index 0000000..1601b4f --- /dev/null +++ b/app/controllers/api/ApiUserController.go @@ -0,0 +1,155 @@ +package api + +import ( + "github.com/revel/revel" + // "encoding/json" + "gopkg.in/mgo.v2/bson" + "github.com/leanote/leanote/app/info" + . "github.com/leanote/leanote/app/lea" + "time" + // "github.com/leanote/leanote/app/types" + "io/ioutil" + // "fmt" + // "math" + "os" + + // "path" + // "strconv" +) + +type ApiUser struct { + ApiBaseContrller +} + +// 获取用户信息 +// [OK] +func (c ApiUser) Info() revel.Result { + re := info.NewApiRe() + + userInfo := c.getUserInfo() + if userInfo.UserId == "" { + return c.RenderJson(re) + } + apiUser := info.ApiUser{ + UserId: userInfo.UserId.Hex(), + Username: userInfo.Username, + Email: userInfo.Email, + Logo: userInfo.Logo, + Verified: userInfo.Verified, + } + return c.RenderJson(apiUser) +} + +// 修改用户名 +// [OK] +func (c ApiUser) UpdateUsername(username string) revel.Result { + re := info.NewApiRe() + if c.GetUsername() == "demo" { + re.Msg = "cannotUpdateDemo" + return c.RenderJson(re) + } + + if re.Ok, re.Msg = Vd("username", username); !re.Ok { + return c.RenderJson(re) + } + + re.Ok, re.Msg = userService.UpdateUsername(c.getUserId(), username) + return c.RenderJson(re) +} + +// 修改密码 +// [OK] +func (c ApiUser) UpdatePwd(oldPwd, pwd string) revel.Result { + re := info.NewApiRe() + if c.GetUsername() == "demo" { + re.Msg = "cannotUpdateDemo" + return c.RenderJson(re) + } + if re.Ok, re.Msg = Vd("password", oldPwd); !re.Ok { + return c.RenderJson(re) + } + if re.Ok, re.Msg = Vd("password", pwd); !re.Ok { + return c.RenderJson(re) + } + re.Ok, re.Msg = userService.UpdatePwd(c.getUserId(), oldPwd, pwd) + return c.RenderJson(re) +} + +// 获得同步状态 +// [OK] +func (c ApiUser) GetSyncState() revel.Result { + ret := bson.M{"LastSyncUsn": userService.GetUsn(c.getUserId()), "LastSyncTime": time.Now().Unix()} + return c.RenderJson(ret) +} + + +// 头像设置 +// 参数file=文件 +// 成功返回{Logo: url} 头像新url +// [OK] +func (c ApiUser) UpdateLogo() revel.Result { + ok, msg, url := c.uploadImage() + + if ok { + ok = userService.UpdateAvatar(c.getUserId(), url) + return c.RenderJson(map[string]string{"Logo": url}) + } else { + re := info.NewApiRe() + re.Msg = msg + return c.RenderJson(re) + } +} + +// 上传图片 +func (c ApiUser) uploadImage() (ok bool, msg, url string) { + var fileUrlPath = "" + ok = false + + file, handel, err := c.Request.FormFile("file") + if err != nil { + return + } + defer file.Close() + // 生成上传路径 + fileUrlPath = "public/upload/" + c.getUserId() + "/images/logo" + + dir := revel.BasePath + "/" + fileUrlPath + err = os.MkdirAll(dir, 0755) + if err != nil { + return + } + // 生成新的文件名 + filename := handel.Filename + + var ext string + + _, ext = SplitFilename(filename) + if ext != ".gif" && ext != ".jpg" && ext != ".png" && ext != ".bmp" && ext != ".jpeg" { + msg = "notImage" + return + } + + filename = NewGuid() + ext + data, err := ioutil.ReadAll(file) + if err != nil { + LogJ(err) + return + } + + // > 5M? + if len(data) > 5*1024*1024 { + msg = "fileIsTooLarge" + return + } + + toPath := dir + "/" + filename + err = ioutil.WriteFile(toPath, data, 0777) + if err != nil { + LogJ(err) + return + } + + ok = true + url = configService.GetSiteUrl() + "/" + fileUrlPath + "/" + filename + return +} diff --git a/app/controllers/api/UserController.go b/app/controllers/api/UserController.go deleted file mode 100644 index 0e384b8..0000000 --- a/app/controllers/api/UserController.go +++ /dev/null @@ -1,27 +0,0 @@ -package api - -import ( - "github.com/revel/revel" -// "encoding/json" -// "gopkg.in/mgo.v2/bson" - . "github.com/leanote/leanote/app/lea" -// "github.com/leanote/leanote/app/info" -// "github.com/leanote/leanote/app/types" -// "io/ioutil" -// "fmt" -// "math" -// "os" -// "path" -// "strconv" -) - -type ApiUser struct { - *revel.Controller -} - -// 修改用户名, 需要重置session -func (c ApiUser) Info() revel.Result { - Log("APIUser"); - return c.RenderTemplate("home/index.html"); -// return nil; -} \ No newline at end of file diff --git a/app/controllers/api/init.go b/app/controllers/api/init.go new file mode 100644 index 0000000..28a9a69 --- /dev/null +++ b/app/controllers/api/init.go @@ -0,0 +1,144 @@ +package api + +import ( + "github.com/leanote/leanote/app/info" + "github.com/leanote/leanote/app/service" +// . "github.com/leanote/leanote/app/lea" + "github.com/revel/revel" + "strings" +) + +var userService *service.UserService +var noteService *service.NoteService +var trashService *service.TrashService +var notebookService *service.NotebookService +var noteContentHistoryService *service.NoteContentHistoryService +var authService *service.AuthService +var shareService *service.ShareService +var blogService *service.BlogService +var tagService *service.TagService +var pwdService *service.PwdService +var tokenService *service.TokenService +var suggestionService *service.SuggestionService +var albumService *service.AlbumService +var noteImageService *service.NoteImageService +var fileService *service.FileService +var attachService *service.AttachService +var configService *service.ConfigService +var emailService *service.EmailService +var sessionService *service.SessionService + +var pageSize = 1000 +var defaultSortField = "UpdatedTime" +var leanoteUserId = "admin" // 不能更改 + +// 状态 +const ( + S_DEFAULT = iota // 0 + S_NOT_LOGIN // 1 + S_WRONG_USERNAME_PASSWORD // 2 + S_WRONG_CAPTCHA // 3 + S_NEED_CAPTCHA // 4 + S_NOT_OPEN_REGISTER // 4 +) + +// 拦截器 +// 不需要拦截的url +var commonUrl = map[string]map[string]bool{"ApiAuth": map[string]bool{"Login": true, + "Register": true, + }, + // 文件的操作也不用登录, userId会从session中获取 + "ApiFile": map[string]bool{"GetImage": true, + "GetAttach": true, + "GetAllAttachs": true, + }, +} + +func needValidate(controller, method string) bool { + // 在里面 + if v, ok := commonUrl[controller]; ok { + // 在commonUrl里 + if _, ok2 := v[method]; ok2 { + return false + } + return true + } else { + // controller不在这里的, 肯定要验证 + return true + } +} + +// 这里得到token, 若不是login, logout等公用操作, 必须验证是否已登录 +func AuthInterceptor(c *revel.Controller) revel.Result { + // 得到token /api/user/info?userId=xxx&token=xxxxx + token := c.Params.Values.Get("token") + noToken := false + if token == "" { + // 若无, 则取sessionId + token = c.Session.Id() + noToken = true + } + c.Session["_token"] = token + + // 全部变成首字大写 + var controller = strings.Title(c.Name) + var method = strings.Title(c.MethodName) + + // 验证是否已登录 + // 通过sessionService判断该token下是否有userId, 并返回userId + userId := sessionService.GetUserId(token) + if noToken && userId == "" { + // 从session中获取, api/file/getImage, api/file/getAttach, api/file/getAllAttach + // 客户端 + userId, _ = c.Session["UserId"]; + } + c.Session["_userId"] = userId + + // 是否需要验证? + if !needValidate(controller, method) { + return nil + } + + if userId != "" { + return nil // 已登录 + } + + // 没有登录, 返回错误的信息, 需要登录 + re := info.NewApiRe() + re.Msg = "NOTLOGIN" + return c.RenderJson(re) +} + +func init() { + // interceptors + revel.InterceptFunc(AuthInterceptor, revel.BEFORE, &ApiAuth{}) + revel.InterceptFunc(AuthInterceptor, revel.BEFORE, &ApiUser{}) + revel.InterceptFunc(AuthInterceptor, revel.BEFORE, &ApiFile{}) + revel.InterceptFunc(AuthInterceptor, revel.BEFORE, &ApiNote{}) + revel.InterceptFunc(AuthInterceptor, revel.BEFORE, &ApiTag{}) + revel.InterceptFunc(AuthInterceptor, revel.BEFORE, &ApiNotebook{}) +} + +// 最外层init.go调用 +// 获取service, 单例 +func InitService() { + notebookService = service.NotebookS + noteService = service.NoteS + noteContentHistoryService = service.NoteContentHistoryS + trashService = service.TrashS + shareService = service.ShareS + userService = service.UserS + tagService = service.TagS + blogService = service.BlogS + tokenService = service.TokenS + noteImageService = service.NoteImageS + fileService = service.FileS + albumService = service.AlbumS + attachService = service.AttachS + pwdService = service.PwdS + suggestionService = service.SuggestionS + authService = service.AuthS + configService = service.ConfigS + emailService = service.EmailS + sessionService = service.SessionS +} diff --git a/app/db/Mgo.go b/app/db/Mgo.go index 684ae10..c062860 100644 --- a/app/db/Mgo.go +++ b/app/db/Mgo.go @@ -28,7 +28,7 @@ var Groups *mgo.Collection var GroupUsers *mgo.Collection var Tags *mgo.Collection -//var TagNotes *mgo.Collection +var NoteTags *mgo.Collection var TagCounts *mgo.Collection var UserBlogs *mgo.Collection @@ -116,7 +116,7 @@ func Init(url, dbname string) { // tag Tags = Session.DB(dbname).C("tags") -// TagNotes = Session.DB(dbname).C("tag_notes") + NoteTags = Session.DB(dbname).C("note_tags") TagCounts = Session.DB(dbname).C("tag_count") // blog diff --git a/app/info/Api.go b/app/info/Api.go new file mode 100644 index 0000000..5b19413 --- /dev/null +++ b/app/info/Api.go @@ -0,0 +1,119 @@ +package info + +import ( + "time" + "gopkg.in/mgo.v2/bson" +) + +//--------- +// 数据结构 +//--------- +type NoteFile struct { + FileId string // 服务器端Id + LocalFileId string // 客户端Id + Type string // images/png, doc, xls, 根据fileName确定 + Title string + HasBody bool // 传过来的值是否要更新内容 + IsAttach bool // 是否是附件, 不是附件就是图片 +} +type ApiNote struct { + NoteId string + NotebookId string + UserId string + Title string + Desc string +// ImgSrc string + Tags []string + Abstract string + Content string + IsMarkdown bool +// FromUserId string // 为共享而新建 + IsBlog bool // 是否是blog, 更新note不需要修改, 添加note时才有可能用到, 此时需要判断notebook是否设为Blog + IsTrash bool + IsDeleted bool + Usn int + Files []NoteFile + CreatedTime time.Time + UpdatedTime time.Time + PublicTime time.Time +} + + +// 内容 +type ApiNoteContent struct { + NoteId bson.ObjectId `bson:"_id,omitempty"` + UserId bson.ObjectId `bson:"UserId"` + + Content string `Content` + +// CreatedTime time.Time `CreatedTime` +// UpdatedTime time.Time `UpdatedTime` +} + +// 转换 +func NoteToApiNote(note Note, files []NoteFile) ApiNote { + apiNote := ApiNote{} + return apiNote +} + +//---------- +// 用户信息 +//---------- + +type ApiUser struct { + UserId string + Username string + Email string + Verified bool + Logo string +} + +//---------- +// Notebook +//---------- +type ApiNotebook struct { + NotebookId bson.ObjectId `bson:"_id,omitempty"` // 必须要设置bson:"_id" 不然mgo不会认为是主键 + UserId bson.ObjectId `bson:"UserId"` + ParentNotebookId bson.ObjectId `bson:"ParentNotebookId,omitempty"` // 上级 + Seq int `Seq` // 排序 + Title string `Title` // 标题 + UrlTitle string `UrlTitle` // Url标题 2014/11.11加 + IsBlog bool `IsBlog,omitempty` // 是否是Blog 2013/12/29 新加 + CreatedTime time.Time `CreatedTime,omitempty` + UpdatedTime time.Time `UpdatedTime,omitempty` + Usn int `Usn` // UpdateSequenceNum + IsDeleted bool `IsDeleted` +} + +//--------- +// api 返回 +//--------- + +// 一般返回 +type ApiRe struct { + Ok bool + Msg string +} + +func NewApiRe() ApiRe { + return ApiRe{Ok: false} +} + +// auth +type AuthOk struct { + Ok bool + Token string + UserId bson.ObjectId + Email string + Username string +} + +// 供notebook, note, tag更新的返回数据用 +type ReUpdate struct { + Ok bool + Msg string + Usn int +} +func NewReUpdate() ReUpdate { + return ReUpdate{Ok: false} +} \ No newline at end of file diff --git a/app/info/BlogCustom.go b/app/info/BlogCustom.go index c309c21..aed4b24 100644 --- a/app/info/BlogCustom.go +++ b/app/info/BlogCustom.go @@ -47,3 +47,11 @@ type Archive struct { MonthAchives []ArchiveMonth Posts []*Post } + +type Cate struct { + CateId string + ParentCateId string + Title string + UrlTitle string + Children []*Cate +} diff --git a/app/info/NoteInfo.go b/app/info/NoteInfo.go index a1bce49..1dd31f5 100644 --- a/app/info/NoteInfo.go +++ b/app/info/NoteInfo.go @@ -34,12 +34,17 @@ type Note struct { IsMarkdown bool `IsMarkdown` // 是否是markdown笔记, 默认是false AttachNum int `AttachNum` // 2014/9/21, attachments num - + CreatedTime time.Time `CreatedTime` UpdatedTime time.Time `UpdatedTime` RecommendTime time.Time `RecommendTime,omitempty` // 推荐时间 PublicTime time.Time `PublicTime,omitempty` // 发表时间, 公开为博客则设置 UpdatedUserId bson.ObjectId `bson:"UpdatedUserId"` // 如果共享了, 并可写, 那么可能是其它他修改了 + + // 2015/1/15, 更新序号 + Usn int `Usn` // UpdateSequenceNum + + IsDeleted bool `IsDeleted` // 删除位 } // 内容 diff --git a/app/info/NotebookInfo.go b/app/info/NotebookInfo.go index 8d0db19..baeb2a0 100644 --- a/app/info/NotebookInfo.go +++ b/app/info/NotebookInfo.go @@ -19,6 +19,10 @@ type Notebook struct { IsBlog bool `IsBlog,omitempty` // 是否是Blog 2013/12/29 新加 CreatedTime time.Time `CreatedTime,omitempty` UpdatedTime time.Time `UpdatedTime,omitempty` + + // 2015/1/15, 更新序号 + Usn int `Usn` // UpdateSequenceNum + IsDeleted bool `IsDeleted` } // 仅仅是为了返回前台 diff --git a/app/info/SessionInfo.go b/app/info/SessionInfo.go index a724626..d08386d 100644 --- a/app/info/SessionInfo.go +++ b/app/info/SessionInfo.go @@ -13,6 +13,8 @@ type Session struct { LoginTimes int `LoginTimes` // 登录错误时间 Captcha string `Captcha` // 验证码 + + UserId string `UserId` // API时有值UserId CreatedTime time.Time `CreatedTime` UpdatedTime time.Time `UpdatedTime` // 更新时间, expire这个时间会自动清空 diff --git a/app/info/TagInfo.go b/app/info/TagInfo.go index 722bd5f..c7e67de 100644 --- a/app/info/TagInfo.go +++ b/app/info/TagInfo.go @@ -2,6 +2,7 @@ package info import ( "gopkg.in/mgo.v2/bson" + "time" ) // 这里主要是为了统计每个tag的note数目 @@ -21,6 +22,18 @@ type Tag struct { Tags []string `Tags` } +// v2 版标签 +type NoteTag struct { + TagId bson.ObjectId `bson:"_id"` + UserId bson.ObjectId `UserId` // 谁的 + Tag string `Tag` // UserId, Tag是唯一索引 + Usn int `Usn` // Update Sequence Number + Count int `Count` // 笔记数 + CreatedTime time.Time `CreatedTime` + UpdatedTime time.Time `UpdatedTime` + IsDeleted bool `IsDeleted` // 删除位 +} + type TagCount struct { TagCountId bson.ObjectId `bson:"_id,omitempty"` UserId bson.ObjectId `UserId` // 谁的 @@ -28,6 +41,7 @@ type TagCount struct { IsBlog bool `IsBlog` // 是否是博客的tag统计 Count int `Count` // 统计数量 } + /* type TagsCounts []TagCount func (this TagsCounts) Len() int { @@ -39,4 +53,4 @@ func (this TagsCounts) Less(i, j int) bool { func (this TagsCounts) Swap(i, j int) { this[i], this[j] = this[j], this[i] } -*/ \ No newline at end of file +*/ diff --git a/app/info/UserInfo.go b/app/info/UserInfo.go index abd06e3..794544e 100644 --- a/app/info/UserInfo.go +++ b/app/info/UserInfo.go @@ -36,22 +36,26 @@ type User struct { ThirdType int `ThirdType` // 第三方类型 // 用户的帐户类型 - - ImageNum int `bson:"ImageNum" json:"-"` // 图片数量 - ImageSize int `bson:"ImageSize" json:"-"` // 图片大小 - AttachNum int `bson:"AttachNum" json:"-"` // 附件数量 - AttachSize int `bson:"AttachSize" json:"-"` // 附件大小 - FromUserId bson.ObjectId `FromUserId,omitempty` // 邀请的用户 + + ImageNum int `bson:"ImageNum" json:"-"` // 图片数量 + ImageSize int `bson:"ImageSize" json:"-"` // 图片大小 + AttachNum int `bson:"AttachNum" json:"-"` // 附件数量 + AttachSize int `bson:"AttachSize" json:"-"` // 附件大小 + FromUserId bson.ObjectId `FromUserId,omitempty` // 邀请的用户 AccountType string `bson:"AccountType" json:"-"` // normal(为空), premium AccountStartTime time.Time `bson:"AccountStartTime" json:"-"` // 开始日期 AccountEndTime time.Time `bson:"AccountEndTime" json:"-"` // 结束日期 // 阈值 - MaxImageNum int `bson:"MaxImageNums" json:"-"` // 图片数量 - MaxImageSize int `bson:"MaxImageSize" json:"-"` // 图片大小 - MaxAttachNum int `bson:"MaxAttachNum" json:"-"` // 图片数量 - MaxAttachSize int `bson:"MaxAttachSize" json:"-"` // 图片大小 - MaxPerAttachSize int `bson:"MaxPerAttachSize" json:"-"` // 单个附件大小 + MaxImageNum int `bson:"MaxImageNums" json:"-"` // 图片数量 + MaxImageSize int `bson:"MaxImageSize" json:"-"` // 图片大小 + MaxAttachNum int `bson:"MaxAttachNum" json:"-"` // 图片数量 + MaxAttachSize int `bson:"MaxAttachSize" json:"-"` // 图片大小 + MaxPerAttachSize int `bson:"MaxPerAttachSize" json:"-"` // 单个附件大小 + + // 2015/1/15, 更新序号 + Usn int `Usn` // UpdateSequenceNum , 全局的 + FullSyncBefore time.Time `bson:"FullSyncBefore"` // 需要全量同步的时间, 如果 > 客户端的LastSyncTime, 则需要全量更新 } type UserAccount struct { @@ -59,11 +63,11 @@ type UserAccount struct { AccountStartTime time.Time `bson:"AccountStartTime" json:"-"` // 开始日期 AccountEndTime time.Time `bson:"AccountEndTime" json:"-"` // 结束日期 // 阈值 - MaxImageNum int `bson:"MaxImageNums" json:"-"` // 图片数量 - MaxImageSize int `bson:"MaxImageSize" json:"-"` // 图片大小 - MaxAttachNum int `bson:"MaxAttachNum" json:"-"` // 图片数量 - MaxAttachSize int `bson:"MaxAttachSize" json:"-"` // 图片大小 - MaxPerAttachSize int `bson:"MaxPerAttachSize" json:"-"` // 单个附件大小 + MaxImageNum int `bson:"MaxImageNums" json:"-"` // 图片数量 + MaxImageSize int `bson:"MaxImageSize" json:"-"` // 图片大小 + MaxAttachNum int `bson:"MaxAttachNum" json:"-"` // 图片数量 + MaxAttachSize int `bson:"MaxAttachSize" json:"-"` // 图片大小 + MaxPerAttachSize int `bson:"MaxPerAttachSize" json:"-"` // 单个附件大小 } // 用户与博客信息结合, 公开 @@ -75,6 +79,6 @@ type UserAndBlog struct { BlogTitle string `BlogTitle` // 博客标题 BlogLogo string `BlogLogo` // 博客Logo BlogUrl string `BlogUrl` // 博客链接, 主页 - + BlogUrls // 各个页面 } diff --git a/app/init.go b/app/init.go index 2c74049..6d4501b 100644 --- a/app/init.go +++ b/app/init.go @@ -6,6 +6,7 @@ import ( "github.com/leanote/leanote/app/service" "github.com/leanote/leanote/app/db" "github.com/leanote/leanote/app/controllers" + "github.com/leanote/leanote/app/controllers/api" "github.com/leanote/leanote/app/controllers/admin" "github.com/leanote/leanote/app/controllers/member" _ "github.com/leanote/leanote/app/lea/binder" @@ -373,5 +374,6 @@ func init() { admin.InitService() member.InitService() service.ConfigS.InitGlobalConfigs() - }); -} \ No newline at end of file + api.InitService() + }) +} diff --git a/app/lea/Util.go b/app/lea/Util.go index 3d28047..64ec2e6 100644 --- a/app/lea/Util.go +++ b/app/lea/Util.go @@ -122,9 +122,44 @@ func ReplaceAll(oldStr, pattern, newStr string) string { return string(s) } +// 获取纯文本 +func SubStringHTMLToRaw(param string, length int) (result string) { + if param == "" { + return "" + } + result = "" + n := 0 + var temp rune // 中文问题, 用rune来解决 + rStr := []rune(param) + isCode := false + for i := 0; i < len(rStr); i++ { + temp = rStr[i] + if temp == '<' { + isCode = true + continue + } else if temp == '>' { + isCode = false + result += " "; // 空格 + continue + } + if !isCode { + result += string(temp) + n++ + if n >= length { + break + } + } + } + return +} + +// 获取摘要, HTML func SubStringHTML(param string, length int, end string) string { - // 先取出
占位.. + if param == "" { + return "" + } + // 先取出占位.. result := "" // 1 @@ -196,6 +231,7 @@ func SubStringHTML(param string, length int, end string) string { return result } + // 是否是合格的密码 func IsGoodPwd(pwd string) (bool, string) { if pwd == "" { @@ -212,7 +248,7 @@ func IsEmail(email string) bool { if email == "" { return false; } - ok, _ := regexp.MatchString(`^([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+\.[0-9a-zA-Z]{2,3}$`, email) + ok, _ := regexp.MatchString(`^([a-zA-Z0-9]+[_|\_|\.|\-]?)*[a-z\-A-Z0-9]+@([a-zA-Z0-9]+[_|\_|\.|\-]?)*[a-zA-Z0-9\-]+\.[0-9a-zA-Z]{2,3}$`, email) return ok } diff --git a/app/lea/binder/binder.go b/app/lea/binder/binder.go index fb06d44..3087478 100644 --- a/app/lea/binder/binder.go +++ b/app/lea/binder/binder.go @@ -4,6 +4,7 @@ import ( "github.com/revel/revel" "github.com/leanote/leanote/app/info" "github.com/leanote/leanote/app/controllers" +// "github.com/leanote/leanote/app/controllers/api" "fmt" "reflect" "strings" @@ -54,15 +55,34 @@ func nextKey(key string) string { return key[:fieldLen] } var leanoteStructBinder = revel.Binder{ + // name == "noteOrContent" Bind: func(params *revel.Params, name string, typ reflect.Type) reflect.Value { - result := reflect.New(typ).Elem() + result := reflect.New(typ).Elem() // 创建一个该类型的, 然后其field从所有的param去取 fieldValues := make(map[string]reflect.Value) - for key, _ := range params.Values { +// fmt.Println(name) + // fmt.Println(typ) // api.NoteFiles + // name = files[0], files[1], noteContent +// fmt.Println(params.Values) + /* +map[Title:[test1] METHOD:[POST] NotebookId:[54c4f51705fcd14031000002] +files[1][FileId]:[] +controller:[note] +files[1][LocalFileId]:[54c7ae27d98d0329dd000000] files[1][HasBody]:[true] files[0][FileId]:[] files[0][LocalFileId]:[54c7ae855e94ea2dba000000] action:[addNote] Content:[lifedddddd